View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  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.ldap;
20  
21  import davmail.AbstractConnection;
22  import davmail.BundleMessage;
23  import davmail.Settings;
24  import davmail.exception.DavMailException;
25  import davmail.exchange.ExchangeSession;
26  import davmail.exchange.ExchangeSessionFactory;
27  import davmail.exchange.dav.DavExchangeSession;
28  import davmail.ui.tray.DavGatewayTray;
29  import davmail.util.IOUtil;
30  
31  import org.apache.log4j.Logger;
32  
33  import javax.naming.InvalidNameException;
34  import javax.naming.ldap.Rdn;
35  import javax.security.auth.callback.Callback;
36  import javax.security.auth.callback.CallbackHandler;
37  import javax.security.auth.callback.NameCallback;
38  import javax.security.auth.callback.PasswordCallback;
39  import javax.security.sasl.AuthorizeCallback;
40  import javax.security.sasl.Sasl;
41  import javax.security.sasl.SaslServer;
42  import java.io.BufferedInputStream;
43  import java.io.BufferedOutputStream;
44  import java.io.ByteArrayOutputStream;
45  import java.io.IOException;
46  import java.net.InetAddress;
47  import java.net.Socket;
48  import java.net.SocketException;
49  import java.net.SocketTimeoutException;
50  import java.net.UnknownHostException;
51  import java.nio.charset.StandardCharsets;
52  import java.text.ParseException;
53  import java.text.SimpleDateFormat;
54  import java.util.ArrayList;
55  import java.util.Calendar;
56  import java.util.HashMap;
57  import java.util.HashSet;
58  import java.util.LinkedHashSet;
59  import java.util.List;
60  import java.util.Map;
61  import java.util.Set;
62  
63  /**
64   * Handle a caldav connection.
65   */
66  public class LdapConnection extends AbstractConnection {
67      private static final Logger LOGGER = Logger.getLogger(LdapConnection.class);
68      /**
69       * Davmail base context
70       */
71      static final String BASE_CONTEXT = "ou=people";
72      /**
73       * OSX server (OpenDirectory) base context
74       */
75      static final String OD_BASE_CONTEXT = "o=od";
76      static final String OD_USER_CONTEXT = "cn=users, o=od";
77      static final String OD_CONFIG_CONTEXT = "cn=config, o=od";
78      static final String COMPUTER_CONTEXT = "cn=computers, o=od";
79      static final String OD_GROUP_CONTEXT = "cn=groups, o=od";
80  
81      // TODO: adjust Directory Utility settings
82      static final String COMPUTER_CONTEXT_LION = "cn=computers,o=od";
83      static final String OD_USER_CONTEXT_LION = "cn=users, ou=people";
84  
85      /**
86       * Root DSE naming contexts (default and OpenDirectory)
87       */
88      static final List<String> NAMING_CONTEXTS = new ArrayList<>();
89  
90      static {
91          NAMING_CONTEXTS.add(BASE_CONTEXT);
92          NAMING_CONTEXTS.add(OD_BASE_CONTEXT);
93      }
94  
95      static final List<String> PERSON_OBJECT_CLASSES = new ArrayList<>();
96  
97      static {
98          PERSON_OBJECT_CLASSES.add("top");
99          PERSON_OBJECT_CLASSES.add("person");
100         PERSON_OBJECT_CLASSES.add("organizationalPerson");
101         PERSON_OBJECT_CLASSES.add("inetOrgPerson");
102         // OpenDirectory class for iCal
103         PERSON_OBJECT_CLASSES.add("apple-user");
104     }
105 
106     /**
107      * Map Exchange contact attribute names to LDAP attributes.
108      * Used only when returningAttributes is empty in LDAP request (return all available attributes)
109      */
110     static final HashMap<String, String> CONTACT_TO_LDAP_ATTRIBUTE_MAP = new HashMap<>();
111 
112     static {
113         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("imapUid", "uid");
114         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("co", "countryname");
115         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute1", "custom1");
116         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute2", "custom2");
117         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute3", "custom3");
118         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("extensionattribute4", "custom4");
119         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("smtpemail1", "mail");
120         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("smtpemail2", "xmozillasecondemail");
121         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeCountry", "mozillahomecountryname");
122         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeCity", "mozillahomelocalityname");
123         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homePostalCode", "mozillahomepostalcode");
124         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeState", "mozillahomestate");
125         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("homeStreet", "mozillahomestreet");
126         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("businesshomepage", "mozillaworkurl");
127         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("nickname", "mozillanickname");
128         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("msexchangecertificate", "msexchangecertificate;binary");
129         CONTACT_TO_LDAP_ATTRIBUTE_MAP.put("usersmimecertificate", "usersmimecertificate;binary");
130     }
131 
132     /**
133      * OSX constant computer guid (used by iCal attendee completion)
134      */
135     static final String COMPUTER_GUID = "52486C30-F0AB-48E3-9C37-37E9B28CDD7B";
136     /**
137      * OSX constant virtual host guid (used by iCal attendee completion)
138      */
139     static final String VIRTUALHOST_GUID = "D6DD8A10-1098-11DE-8C30-0800200C9A66";
140 
141     /**
142      * OSX constant value for attribute apple-serviceslocator
143      */
144     static final HashMap<String, String> STATIC_ATTRIBUTE_MAP = new HashMap<>();
145 
146     static {
147         STATIC_ATTRIBUTE_MAP.put("apple-serviceslocator", COMPUTER_GUID + ':' + VIRTUALHOST_GUID + ":calendar");
148     }
149 
150     /**
151      * LDAP to Exchange Criteria Map
152      */
153     // TODO: remove
154     static final HashMap<String, String> CRITERIA_MAP = new HashMap<>();
155 
156     static {
157         // assume mail starts with firstname
158         CRITERIA_MAP.put("uid", "AN");
159         CRITERIA_MAP.put("mail", "FN");
160         CRITERIA_MAP.put("displayname", "DN");
161         CRITERIA_MAP.put("cn", "DN");
162         CRITERIA_MAP.put("givenname", "FN");
163         CRITERIA_MAP.put("sn", "LN");
164         CRITERIA_MAP.put("title", "TL");
165         CRITERIA_MAP.put("company", "CP");
166         CRITERIA_MAP.put("o", "CP");
167         CRITERIA_MAP.put("l", "OF");
168         CRITERIA_MAP.put("department", "DP");
169         CRITERIA_MAP.put("apple-group-realname", "DP");
170     }
171 
172     /**
173      * LDAP to Exchange contact attribute map.
174      */
175     static final HashMap<String, String> LDAP_TO_CONTACT_ATTRIBUTE_MAP = new HashMap<>();
176 
177     static {
178         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("uid", "imapUid");
179         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mail", "smtpemail1");
180 
181         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("displayname", "cn");
182         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("commonname", "cn");
183 
184         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("givenname", "givenName");
185 
186         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("surname", "sn");
187 
188         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("company", "o");
189 
190         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-group-realname", "department");
191         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomelocalityname", "homeCity");
192 
193         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("c", "co");
194         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("countryname", "co");
195 
196         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom1", "extensionattribute1");
197         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom2", "extensionattribute2");
198         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom3", "extensionattribute3");
199         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("custom4", "extensionattribute4");
200 
201         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom1", "extensionattribute1");
202         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom2", "extensionattribute2");
203         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom3", "extensionattribute3");
204         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillacustom4", "extensionattribute4");
205 
206         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("telephonenumber", "telephoneNumber");
207         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("orgunit", "department");
208         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("departmentnumber", "department");
209         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("ou", "department");
210 
211         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillaworkstreet2", null);
212         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomestreet", "homeStreet");
213 
214         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("xmozillanickname", "nickname");
215         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillanickname", "nickname");
216 
217         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("cellphone", "mobile");
218         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homeurl", "personalHomePage");
219         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomeurl", "personalHomePage");
220         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-user-homeurl", "personalHomePage");
221         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomepostalcode", "homePostalCode");
222         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("fax", "facsimiletelephonenumber");
223         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomecountryname", "homeCountry");
224         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("streetaddress", "street");
225         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillaworkurl", "businesshomepage");
226         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("workurl", "businesshomepage");
227 
228         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("region", "st");
229 
230         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("birthmonth", "bday");
231         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("birthday", "bday");
232         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("birthyear", "bday");
233 
234         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("carphone", "othermobile");
235         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("nsaimid", "im");
236         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("nscpaimscreenname", "im");
237         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-imhandle", "im");
238         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("imhandle", "im");
239 
240         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("xmozillasecondemail", "smtpemail2");
241 
242         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("notes", "description");
243         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("pagerphone", "pager");
244         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("pager", "pager");
245 
246         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("locality", "l");
247         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homephone", "homePhone");
248         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillasecondemail", "smtpemail2");
249 
250         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("zip", "postalcode");
251         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomestate", "homeState");
252 
253         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("modifytimestamp", "lastmodified");
254 
255         // ignore attribute
256         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("objectclass", null);
257         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillausehtmlmail", null);
258         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("xmozillausehtmlmail", null);
259 
260         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mozillahomestreet2", null);
261 
262         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("labeleduri", null);
263         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-generateduid", null);
264         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("uidnumber", null);
265         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("gidnumber", null);
266         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("jpegphoto", null);
267         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-emailcontacts", null);
268         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-user-picture", null);
269 
270         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_usercertificate", null);
271         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_realname", null);
272         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_jpegphoto", null);
273         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_guest", null);
274         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_linkedidentity", null);
275         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_defaultlanguage", null);
276         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_hint", null);
277         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers__defaultlanguage", null);
278         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("_writers_picture", null);
279 
280         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-user-authenticationhint", null);
281         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("external", null);
282         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("userpassword", null);
283         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("linkedidentity", null);
284         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homedirectory", null);
285         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("authauthority", null);
286 
287         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("applefloor", null);
288         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("buildingname", null);
289         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("destinationindicator", null);
290         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("postaladdress", null);
291         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("homepostaladdress", null);
292 
293         // iCal search attribute
294         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("apple-serviceslocator", "apple-serviceslocator");
295 
296         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("msexchangecertificate;binary", "msexchangecertificate");
297         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("usersmimecertificate;binary", "usersmimecertificate");
298 
299         // Graph specific attributes
300         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("userprincipalname", "userprincipalname");
301         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("mailboxtype", "mailboxtype");
302         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("persontype", "persontype");
303         LDAP_TO_CONTACT_ATTRIBUTE_MAP.put("isfavorite", "isfavorite");
304     }
305 
306     /**
307      * LDAP filter attributes ignore map
308      */
309     // TODO remove
310     static final HashSet<String> IGNORE_MAP = new HashSet<>();
311 
312     static {
313         IGNORE_MAP.add("objectclass");
314         IGNORE_MAP.add("apple-generateduid");
315         IGNORE_MAP.add("augmentconfiguration");
316         IGNORE_MAP.add("ou");
317         IGNORE_MAP.add("apple-realname");
318         IGNORE_MAP.add("apple-group-nestedgroup");
319         IGNORE_MAP.add("apple-group-memberguid");
320         IGNORE_MAP.add("macaddress");
321         IGNORE_MAP.add("memberuid");
322     }
323 
324     // LDAP version
325     // static final int LDAP_VERSION2 = 0x02;
326     static final int LDAP_VERSION3 = 0x03;
327 
328     // LDAP request operations
329     static final int LDAP_REQ_BIND = 0x60;
330     static final int LDAP_REQ_SEARCH = 0x63;
331     static final int LDAP_REQ_UNBIND = 0x42;
332     static final int LDAP_REQ_ABANDON = 0x50;
333 
334     // LDAP response operations
335     static final int LDAP_REP_BIND = 0x61;
336     static final int LDAP_REP_SEARCH = 0x64;
337     static final int LDAP_REP_RESULT = 0x65;
338 
339     static final int LDAP_SASL_BIND_IN_PROGRESS = 0x0E;
340 
341     // LDAP return codes
342     static final int LDAP_OTHER = 80;
343     static final int LDAP_SUCCESS = 0;
344     static final int LDAP_SIZE_LIMIT_EXCEEDED = 4;
345     static final int LDAP_INVALID_CREDENTIALS = 49;
346 
347     // LDAP filter code
348     static final int LDAP_FILTER_AND = 0xa0;
349     static final int LDAP_FILTER_OR = 0xa1;
350     static final int LDAP_FILTER_NOT = 0xa2;
351 
352     // LDAP filter operators
353     static final int LDAP_FILTER_SUBSTRINGS = 0xa4;
354     //static final int LDAP_FILTER_GE = 0xa5;
355     //static final int LDAP_FILTER_LE = 0xa6;
356     static final int LDAP_FILTER_PRESENT = 0x87;
357     //static final int LDAP_FILTER_APPROX = 0xa8;
358     static final int LDAP_FILTER_EQUALITY = 0xa3;
359 
360     // LDAP filter mode
361     static final int LDAP_SUBSTRING_INITIAL = 0x80;
362     static final int LDAP_SUBSTRING_ANY = 0x81;
363     static final int LDAP_SUBSTRING_FINAL = 0x82;
364 
365     // BER data types
366     static final int LBER_ENUMERATED = 0x0a;
367     static final int LBER_SET = 0x31;
368     static final int LBER_SEQUENCE = 0x30;
369 
370     // LDAP search scope
371     static final int SCOPE_BASE_OBJECT = 0;
372     //static final int SCOPE_ONE_LEVEL = 1;
373     //static final int SCOPE_SUBTREE = 2;
374 
375     /**
376      * Sasl server for DIGEST-MD5 authentication
377      */
378     protected SaslServer saslServer;
379 
380     /**
381      * raw connection inputStream
382      */
383     protected BufferedInputStream is;
384 
385     /**
386      * reusable BER encoder
387      */
388     protected final BerEncoder responseBer = new BerEncoder();
389 
390     /**
391      * Current LDAP version (used for String encoding)
392      */
393     int ldapVersion = LDAP_VERSION3;
394 
395     /**
396      * Search threads map
397      */
398     protected final HashMap<Integer, SearchRunnable> searchThreadMap = new HashMap<>();
399 
400     /**
401      * Initialize the streams and start the thread.
402      *
403      * @param clientSocket LDAP client socket
404      */
405     public LdapConnection(Socket clientSocket) {
406         super(LdapConnection.class.getSimpleName(), clientSocket);
407         try {
408             is = new BufferedInputStream(client.getInputStream());
409             os = new BufferedOutputStream(client.getOutputStream());
410         } catch (IOException e) {
411             close();
412             DavGatewayTray.error(new BundleMessage("LOG_EXCEPTION_GETTING_SOCKET_STREAMS"), e);
413         }
414     }
415 
416     protected boolean isLdapV3() {
417         return ldapVersion == LDAP_VERSION3;
418     }
419 
420     @Override
421     public void run() {
422         byte[] inbuf = new byte[2048];   // Buffer for reading incoming bytes
423         int bytesread;  // Number of bytes in inbuf
424         int bytesleft;  // Number of bytes that need to read for completing resp
425         int br;         // Temp; number of bytes read from stream
426         int offset;     // Offset of where to store bytes in inbuf
427         boolean eos;    // End of stream
428 
429         try {
430             ExchangeSessionFactory.checkConfig();
431             while (true) {
432                 offset = 0;
433 
434                 // check that it is the beginning of a sequence
435                 bytesread = is.read(inbuf, offset, 1);
436                 if (bytesread < 0) {
437                     break; // EOF
438                 }
439 
440                 if (inbuf[offset++] != (Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR)) {
441                     continue;
442                 }
443 
444                 // get length of sequence
445                 bytesread = is.read(inbuf, offset, 1);
446                 if (bytesread < 0) {
447                     break; // EOF
448                 }
449                 int seqlen = inbuf[offset++]; // Length of ASN sequence
450 
451                 // if high bit is on, length is encoded in the
452                 // subsequent length bytes and the number of length bytes
453                 // is equal to & 0x80 (i.e. length byte with high bit off).
454                 if ((seqlen & 0x80) == 0x80) {
455                     int seqlenlen = seqlen & 0x7f;  // number of length bytes
456 
457                     bytesread = 0;
458                     eos = false;
459 
460                     // Read all length bytes
461                     while (bytesread < seqlenlen) {
462                         br = is.read(inbuf, offset + bytesread,
463                                 seqlenlen - bytesread);
464                         if (br < 0) {
465                             eos = true;
466                             break; // EOF
467                         }
468                         bytesread += br;
469                     }
470 
471                     // end-of-stream reached before length bytes are read
472                     if (eos) {
473                         break;  // EOF
474                     }
475 
476                     // Add contents of length bytes to determine length
477                     seqlen = 0;
478                     for (int i = 0; i < seqlenlen; i++) {
479                         seqlen = (seqlen << 8) + (inbuf[offset + i] & 0xff);
480                     }
481                     offset += bytesread;
482                 }
483 
484                 // read in seqlen bytes
485                 bytesleft = seqlen;
486                 if ((offset + bytesleft) > inbuf.length) {
487                     byte[] nbuf = new byte[offset + bytesleft];
488                     System.arraycopy(inbuf, 0, nbuf, 0, offset);
489                     inbuf = nbuf;
490                 }
491                 while (bytesleft > 0) {
492                     bytesread = is.read(inbuf, offset, bytesleft);
493                     if (bytesread < 0) {
494                         break; // EOF
495                     }
496                     offset += bytesread;
497                     bytesleft -= bytesread;
498                 }
499 
500                 DavGatewayTray.switchIcon();
501 
502                 handleRequest(inbuf, offset);
503             }
504 
505         } catch (SocketException e) {
506             DavGatewayTray.debug(new BundleMessage("LOG_CONNECTION_CLOSED"));
507         } catch (SocketTimeoutException e) {
508             DavGatewayTray.debug(new BundleMessage("LOG_CLOSE_CONNECTION_ON_TIMEOUT"));
509         } catch (Exception e) {
510             DavGatewayTray.log(e);
511             try {
512                 sendErr(0, LDAP_REP_BIND, e);
513             } catch (IOException e2) {
514                 DavGatewayTray.warn(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
515             }
516         } finally {
517             // cancel all search threads
518             synchronized (searchThreadMap) {
519                 for (SearchRunnable searchRunnable : searchThreadMap.values()) {
520                     searchRunnable.abandon();
521                 }
522             }
523             close();
524         }
525         DavGatewayTray.resetIcon();
526     }
527 
528     protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
529 
530     protected void handleRequest(byte[] inbuf, int offset) throws IOException {
531         //dumpBer(inbuf, offset);
532         BerDecoder reqBer = new BerDecoder(inbuf, 0, offset);
533         int currentMessageId = 0;
534         try {
535             reqBer.parseSeq(null);
536             currentMessageId = reqBer.parseInt();
537             int requestOperation = reqBer.peekByte();
538 
539             if (requestOperation == LDAP_REQ_BIND) {
540                 reqBer.parseSeq(null);
541                 ldapVersion = reqBer.parseInt();
542                 // check for dn authentication
543                 userName = extractRdnValue(reqBer.parseString(isLdapV3()));
544                 if (reqBer.peekByte() == (Ber.ASN_CONTEXT | Ber.ASN_CONSTRUCTOR | 3)) {
545                     // SASL authentication
546                     reqBer.parseSeq(null);
547                     // Get mechanism, usually DIGEST-MD5
548                     String mechanism = reqBer.parseString(isLdapV3());
549 
550                     byte[] serverResponse;
551                     CallbackHandler callbackHandler = callbacks -> {
552                         // look for username in callbacks
553                         for (Callback callback : callbacks) {
554                             if (callback instanceof NameCallback) {
555                                 userName = extractRdnValue(((NameCallback) callback).getDefaultName());
556                                 // get password from session pool
557                                 password = ExchangeSessionFactory.getUserPassword(userName);
558                             }
559                         }
560                         // handle other callbacks
561                         for (Callback callback : callbacks) {
562                             if (callback instanceof AuthorizeCallback) {
563                                 ((AuthorizeCallback) callback).setAuthorized(true);
564                             } else if (callback instanceof PasswordCallback) {
565                                 if (password != null) {
566                                     ((PasswordCallback) callback).setPassword(password.toCharArray());
567                                 }
568                             }
569                         }
570                     };
571                     int status;
572                     if (reqBer.bytesLeft() > 0 && saslServer != null) {
573                         byte[] clientResponse = reqBer.parseOctetString(Ber.ASN_OCTET_STR, null);
574                         serverResponse = saslServer.evaluateResponse(clientResponse);
575                         status = LDAP_SUCCESS;
576 
577                         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_USER", currentMessageId, userName));
578                         try {
579                             session = ExchangeSessionFactory.getInstance(userName, password);
580                             logConnection("LOGON", userName);
581                             DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_SUCCESS"));
582                         } catch (IOException e) {
583                             logConnection("FAILED", userName);
584                             serverResponse = EMPTY_BYTE_ARRAY;
585                             status = LDAP_INVALID_CREDENTIALS;
586                             DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_INVALID_CREDENTIALS"));
587                         }
588 
589                     } else {
590                         Map<String, String> properties = new HashMap<>();
591                         properties.put("javax.security.sasl.qop", "auth,auth-int");
592                         saslServer = Sasl.createSaslServer(mechanism, "ldap", client.getLocalAddress().getHostAddress(), properties, callbackHandler);
593                         if (saslServer == null) {
594                             throw new IOException("Unable to create SASL server for mechanism " + mechanism);
595                         }
596                         serverResponse = saslServer.evaluateResponse(EMPTY_BYTE_ARRAY);
597                         status = LDAP_SASL_BIND_IN_PROGRESS;
598                     }
599 
600                     responseBer.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
601                     responseBer.encodeInt(currentMessageId);
602                     responseBer.beginSeq(LDAP_REP_BIND);
603                     responseBer.encodeInt(status, LBER_ENUMERATED);
604                     // server credentials
605                     responseBer.encodeString("", isLdapV3());
606                     responseBer.encodeString("", isLdapV3());
607                     // challenge or response
608                     if (serverResponse != null) {
609                         responseBer.encodeOctetString(serverResponse, 0x87);
610                     }
611                     responseBer.endSeq();
612                     responseBer.endSeq();
613                     sendResponse();
614 
615                 } else {
616                     password = reqBer.parseStringWithTag(Ber.ASN_CONTEXT, isLdapV3(), null);
617 
618                     if (!userName.isEmpty() && !password.isEmpty()) {
619                         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_USER", currentMessageId, userName));
620                         try {
621                             session = ExchangeSessionFactory.getInstance(userName, password);
622                             logConnection("LOGON", userName);
623                             DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_SUCCESS"));
624                             sendClient(currentMessageId, LDAP_REP_BIND, LDAP_SUCCESS, "");
625                         } catch (IOException e) {
626                             logConnection("FAILED", userName);
627                             DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_INVALID_CREDENTIALS"));
628                             sendClient(currentMessageId, LDAP_REP_BIND, LDAP_INVALID_CREDENTIALS, "");
629                         }
630                     } else {
631                         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_BIND_ANONYMOUS", currentMessageId));
632                         // anonymous bind
633                         sendClient(currentMessageId, LDAP_REP_BIND, LDAP_SUCCESS, "");
634                     }
635                 }
636 
637             } else if (requestOperation == LDAP_REQ_UNBIND) {
638                 DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_UNBIND", currentMessageId));
639                 if (session != null) {
640                     session = null;
641                 }
642             } else if (requestOperation == LDAP_REQ_SEARCH) {
643                 reqBer.parseSeq(null);
644                 String dn = reqBer.parseString(isLdapV3());
645                 int scope = reqBer.parseEnumeration();
646                 /*int derefAliases =*/
647                 reqBer.parseEnumeration();
648                 int sizeLimit = reqBer.parseInt();
649                 if (sizeLimit > 100 || sizeLimit == 0) {
650                     sizeLimit = 100;
651                 }
652                 int timelimit = reqBer.parseInt();
653                 /*boolean typesOnly =*/
654                 reqBer.parseBoolean();
655                 LdapFilter ldapFilter = parseFilter(reqBer);
656                 Set<String> returningAttributes = parseReturningAttributes(reqBer);
657                 SearchRunnable searchRunnable = new SearchRunnable(currentMessageId, dn, scope, sizeLimit, timelimit, ldapFilter, returningAttributes);
658                 if (BASE_CONTEXT.equalsIgnoreCase(dn) || OD_USER_CONTEXT.equalsIgnoreCase(dn) || OD_USER_CONTEXT_LION.equalsIgnoreCase(dn)) {
659                     // launch search in a separate thread
660                     synchronized (searchThreadMap) {
661                         searchThreadMap.put(currentMessageId, searchRunnable);
662                     }
663                     Thread searchThread = new Thread(searchRunnable);
664                     searchThread.setName(getName() + "-Search-" + currentMessageId);
665                     searchThread.start();
666                 } else {
667                     // no need to create a separate thread, just run
668                     searchRunnable.run();
669                 }
670 
671             } else if (requestOperation == LDAP_REQ_ABANDON) {
672                 int abandonMessageId;
673                 abandonMessageId = reqBer.parseIntWithTag(LDAP_REQ_ABANDON);
674                 synchronized (searchThreadMap) {
675                     SearchRunnable searchRunnable = searchThreadMap.get(abandonMessageId);
676                     if (searchRunnable != null) {
677                         searchRunnable.abandon();
678                         searchThreadMap.remove(currentMessageId);
679                     }
680                 }
681                 DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_ABANDON_SEARCH", currentMessageId, abandonMessageId));
682             } else {
683                 DavGatewayTray.debug(new BundleMessage("LOG_LDAP_UNSUPPORTED_OPERATION", requestOperation));
684                 sendClient(currentMessageId, LDAP_REP_RESULT, LDAP_OTHER, "Unsupported operation");
685             }
686         } catch (IOException e) {
687             dumpBer(inbuf, offset);
688             try {
689                 sendErr(currentMessageId, LDAP_REP_RESULT, e);
690             } catch (IOException e2) {
691                 DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
692             }
693             throw e;
694         }
695     }
696 
697     /**
698      * Extract rdn value from username
699      * @param dn distinguished name or username
700      * @return username
701      */
702     private String extractRdnValue(String dn) throws IOException {
703         if (dn.startsWith("uid=")) {
704             String rdn = dn;
705             if (rdn.indexOf(',') > 0) {
706                 rdn = rdn.substring(0, rdn.indexOf(','));
707             }
708             try {
709                 return (String) new Rdn(rdn).getValue();
710             } catch (InvalidNameException e) {
711                 throw new IOException(e);
712             }
713         } else {
714             return dn;
715         }
716     }
717 
718     protected void dumpBer(byte[] inbuf, int offset) {
719         ByteArrayOutputStream baos = new ByteArrayOutputStream();
720         Ber.dumpBER(baos, "LDAP request buffer\n", inbuf, 0, offset);
721         LOGGER.debug(new String(baos.toByteArray(), StandardCharsets.UTF_8));
722     }
723 
724     protected LdapFilter parseFilter(BerDecoder reqBer) throws IOException {
725         LdapFilter ldapFilter;
726         if (reqBer.peekByte() == LDAP_FILTER_PRESENT) {
727             String attributeName = reqBer.parseStringWithTag(LDAP_FILTER_PRESENT, isLdapV3(), null).toLowerCase();
728             ldapFilter = new SimpleFilter(attributeName);
729         } else {
730             int[] seqSize = new int[1];
731             int ldapFilterType = reqBer.parseSeq(seqSize);
732             int end = reqBer.getParsePosition() + seqSize[0];
733 
734             ldapFilter = parseNestedFilter(reqBer, ldapFilterType, end);
735         }
736 
737         return ldapFilter;
738     }
739 
740     protected LdapFilter parseNestedFilter(BerDecoder reqBer, int ldapFilterType, int end) throws IOException {
741         LdapFilter nestedFilter;
742 
743         if ((ldapFilterType == LDAP_FILTER_OR) || (ldapFilterType == LDAP_FILTER_AND)
744                 || ldapFilterType == LDAP_FILTER_NOT) {
745             nestedFilter = new CompoundFilter(ldapFilterType);
746 
747             while (reqBer.getParsePosition() < end && reqBer.bytesLeft() > 0) {
748                 if (reqBer.peekByte() == LDAP_FILTER_PRESENT) {
749                     String attributeName = reqBer.parseStringWithTag(LDAP_FILTER_PRESENT, isLdapV3(), null).toLowerCase();
750                     nestedFilter.add(new SimpleFilter(attributeName));
751                 } else {
752                     int[] seqSize = new int[1];
753                     int ldapFilterOperator = reqBer.parseSeq(seqSize);
754                     int subEnd = reqBer.getParsePosition() + seqSize[0];
755                     nestedFilter.add(parseNestedFilter(reqBer, ldapFilterOperator, subEnd));
756                 }
757             }
758         } else {
759             // simple filter
760             nestedFilter = parseSimpleFilter(reqBer, ldapFilterType);
761         }
762 
763         return nestedFilter;
764     }
765 
766     protected LdapFilter parseSimpleFilter(BerDecoder reqBer, int ldapFilterOperator) throws IOException {
767         String attributeName = reqBer.parseString(isLdapV3()).toLowerCase();
768         int ldapFilterMode = 0;
769 
770         StringBuilder value = new StringBuilder();
771         if (ldapFilterOperator == LDAP_FILTER_SUBSTRINGS) {
772             // Thunderbird sends values with space as separate strings, rebuild value
773             int[] seqSize = new int[1];
774             /*LBER_SEQUENCE*/
775             reqBer.parseSeq(seqSize);
776             int end = reqBer.getParsePosition() + seqSize[0];
777             while (reqBer.getParsePosition() < end && reqBer.bytesLeft() > 0) {
778                 ldapFilterMode = reqBer.peekByte();
779                 if (value.length() > 0) {
780                     value.append(' ');
781                 }
782                 value.append(reqBer.parseStringWithTag(ldapFilterMode, isLdapV3(), null));
783             }
784         } else if (ldapFilterOperator == LDAP_FILTER_EQUALITY) {
785             value.append(reqBer.parseString(isLdapV3()));
786         } else {
787             DavGatewayTray.warn(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER_VALUE"));
788         }
789 
790         String sValue = value.toString();
791 
792         if ("uid".equalsIgnoreCase(attributeName) && sValue.equals(userName)) {
793             // replace with actual alias instead of login name search, only in Dav mode
794             if (session instanceof DavExchangeSession) {
795                 sValue = session.getAlias();
796                 DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REPLACED_UID_FILTER", userName, sValue));
797             }
798         }
799 
800         return new SimpleFilter(attributeName, sValue, ldapFilterOperator, ldapFilterMode);
801     }
802 
803     protected Set<String> parseReturningAttributes(BerDecoder reqBer) throws IOException {
804         Set<String> returningAttributes = new HashSet<>();
805         int[] seqSize = new int[1];
806         reqBer.parseSeq(seqSize);
807         int end = reqBer.getParsePosition() + seqSize[0];
808         while (reqBer.getParsePosition() < end && reqBer.bytesLeft() > 0) {
809             returningAttributes.add(reqBer.parseString(isLdapV3()).toLowerCase());
810         }
811         return returningAttributes;
812     }
813 
814     /**
815      * Send Root DSE
816      *
817      * @param currentMessageId current message id
818      * @throws IOException on error
819      */
820     protected void sendRootDSE(int currentMessageId) throws IOException {
821         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_SEND_ROOT_DSE"));
822 
823         Map<String, Object> attributes = new HashMap<>();
824         attributes.put("objectClass", "top");
825         attributes.put("namingContexts", NAMING_CONTEXTS);
826         //attributes.put("supportedsaslmechanisms", "PLAIN");
827 
828         sendEntry(currentMessageId, "Root DSE", attributes);
829     }
830 
831     protected void addIf(Map<String, Object> attributes, Set<String> returningAttributes, String name, Object value) {
832         if ((returningAttributes.isEmpty()) || returningAttributes.contains(name)) {
833             attributes.put(name, value);
834         }
835     }
836 
837     protected String currentHostName;
838 
839     protected String getCurrentHostName() throws UnknownHostException {
840         if (currentHostName == null) {
841             InetAddress clientInetAddress = client.getInetAddress();
842             if (clientInetAddress != null && clientInetAddress.isLoopbackAddress()) {
843                 // local address, probably using localhost in iCal URL
844                 currentHostName = "localhost";
845             } else {
846                 // remote address, send fully qualified domain name
847                 currentHostName = InetAddress.getLocalHost().getCanonicalHostName();
848             }
849         }
850         return currentHostName;
851     }
852 
853     /**
854      * Cache serviceInfo string value
855      */
856     protected String serviceInfo;
857 
858     protected String getServiceInfo() throws UnknownHostException {
859         if (serviceInfo == null) {
860             serviceInfo = ("<?xml version='1.0' encoding='UTF-8'?>" +
861                     "<!DOCTYPE plist PUBLIC '-//Apple//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>" +
862                     "<plist version='1.0'>" +
863                     "<dict>" +
864                     "<key>com.apple.macosxserver.host</key>" +
865                     "<array>" +
866                     "<string>localhost</string>" +        // NOTE: Will be replaced by real hostname
867                     "</array>" +
868                     "<key>com.apple.macosxserver.virtualhosts</key>" +
869                     "<dict>" +
870                     "<key>" + VIRTUALHOST_GUID + "</key>" +
871                     "<dict>" +
872                     "<key>hostDetails</key>" +
873                     "<dict>" +
874                     "<key>http</key>" +
875                     "<dict>" +
876                     "<key>enabled</key>" +
877                     "<true/>" +
878                     "<key>port</key>" +
879                     "<integer>") + Settings.getProperty("davmail.caldavPort") + "</integer>" +
880                     "</dict>" +
881                     "<key>https</key>" +
882                     "<dict>" +
883                     "<key>disabled</key>" +
884                     "<false/>" +
885                     "<key>port</key>" +
886                     "<integer>0</integer>" +
887                     "</dict>" +
888                     "</dict>" +
889                     "<key>hostname</key>" +
890                     "<string>" + getCurrentHostName() + "</string>" +
891                     "<key>serviceInfo</key>" +
892                     "<dict>" +
893                     "<key>calendar</key>" +
894                     "<dict>" +
895                     "<key>enabled</key>" +
896                     "<true/>" +
897                     "<key>templates</key>" +
898                     "<dict>" +
899                     "<key>calendarUserAddresses</key>" +
900                     "<array>" +
901                     "<string>%(principaluri)s</string>" +
902                     "<string>mailto:%(email)s</string>" +
903                     "<string>urn:uuid:%(guid)s</string>" +
904                     "</array>" +
905                     "<key>principalPath</key>" +
906                     "<string>/principals/__uuids__/%(guid)s/</string>" +
907                     "</dict>" +
908                     "</dict>" +
909                     "</dict>" +
910                     "<key>serviceType</key>" +
911                     "<array>" +
912                     "<string>calendar</string>" +
913                     "</array>" +
914                     "</dict>" +
915                     "</dict>" +
916                     "</dict>" +
917                     "</plist>";
918         }
919         return serviceInfo;
920     }
921 
922     /**
923      * Send ComputerContext
924      *
925      * @param currentMessageId    current message id
926      * @param returningAttributes attributes to return
927      * @throws IOException on error
928      */
929     protected void sendComputerContext(int currentMessageId, Set<String> returningAttributes) throws IOException {
930         List<String> objectClasses = new ArrayList<>();
931         objectClasses.add("top");
932         objectClasses.add("apple-computer");
933         Map<String, Object> attributes = new HashMap<>();
934         addIf(attributes, returningAttributes, "objectClass", objectClasses);
935         addIf(attributes, returningAttributes, "apple-generateduid", COMPUTER_GUID);
936         addIf(attributes, returningAttributes, "apple-serviceinfo", getServiceInfo());
937         // TODO: remove ?
938         addIf(attributes, returningAttributes, "apple-xmlplist", getServiceInfo());
939         addIf(attributes, returningAttributes, "apple-serviceslocator", "::anyService");
940         addIf(attributes, returningAttributes, "cn", getCurrentHostName());
941 
942         String dn = "cn=" + getCurrentHostName() + ", " + COMPUTER_CONTEXT;
943         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_SEND_COMPUTER_CONTEXT", dn, attributes));
944 
945         sendEntry(currentMessageId, dn, attributes);
946     }
947 
948     /**
949      * Send Base Context
950      *
951      * @param currentMessageId current message id
952      * @throws IOException on error
953      */
954     protected void sendBaseContext(int currentMessageId) throws IOException {
955         List<String> objectClasses = new ArrayList<>();
956         objectClasses.add("top");
957         objectClasses.add("organizationalUnit");
958         Map<String, Object> attributes = new HashMap<>();
959         attributes.put("objectClass", objectClasses);
960         attributes.put("description", "DavMail Gateway LDAP for " + Settings.getProperty("davmail.url", Settings.getO365Url()));
961         sendEntry(currentMessageId, BASE_CONTEXT, attributes);
962     }
963 
964     protected void sendEntry(int currentMessageId, String dn, Map<String, Object> attributes) throws IOException {
965         // synchronize on responseBer
966         synchronized (responseBer) {
967             responseBer.reset();
968             responseBer.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
969             responseBer.encodeInt(currentMessageId);
970             responseBer.beginSeq(LDAP_REP_SEARCH);
971             responseBer.encodeString(dn, isLdapV3());
972             responseBer.beginSeq(LBER_SEQUENCE);
973             for (Map.Entry<String, Object> entry : attributes.entrySet()) {
974                 responseBer.beginSeq(LBER_SEQUENCE);
975                 responseBer.encodeString(entry.getKey(), isLdapV3());
976                 responseBer.beginSeq(LBER_SET);
977                 Object values = entry.getValue();
978                 if (values instanceof String) {
979                     responseBer.encodeString((String) values, isLdapV3());
980                 } else if (values instanceof List) {
981                     for (Object value : (Iterable) values) {
982                         responseBer.encodeString((String) value, isLdapV3());
983                     }
984                 } else if (values instanceof byte[]) {
985                     responseBer.encodeOctetString((byte[])values, BerEncoder.ASN_OCTET_STR);
986                 } else {
987                     throw new DavMailException("EXCEPTION_UNSUPPORTED_VALUE", values);
988                 }
989                 responseBer.endSeq();
990                 responseBer.endSeq();
991             }
992             responseBer.endSeq();
993             responseBer.endSeq();
994             responseBer.endSeq();
995             sendResponse();
996         }
997     }
998 
999     protected void sendErr(int currentMessageId, int responseOperation, Exception e) throws IOException {
1000         String message = e.getMessage();
1001         if (message == null) {
1002             message = e.toString();
1003         }
1004         sendClient(currentMessageId, responseOperation, LDAP_OTHER, message);
1005     }
1006 
1007     protected void sendClient(int currentMessageId, int responseOperation, int status, String message) throws IOException {
1008         responseBer.reset();
1009 
1010         responseBer.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
1011         responseBer.encodeInt(currentMessageId);
1012         responseBer.beginSeq(responseOperation);
1013         responseBer.encodeInt(status, LBER_ENUMERATED);
1014         // dn
1015         responseBer.encodeString("", isLdapV3());
1016         // error message
1017         responseBer.encodeString(message, isLdapV3());
1018         responseBer.endSeq();
1019         responseBer.endSeq();
1020         sendResponse();
1021     }
1022 
1023     protected void sendResponse() throws IOException {
1024         //Ber.dumpBER(System.out, ">\n", responseBer.getBuf(), 0, responseBer.getDataLen());
1025         os.write(responseBer.getBuf(), 0, responseBer.getDataLen());
1026         os.flush();
1027     }
1028 
1029     public interface LdapFilter {
1030         ExchangeSession.Condition getContactSearchFilter();
1031 
1032         Map<String, ExchangeSession.Contact> findInGAL(ExchangeSession session, Set<String> returningAttributes, int sizeLimit) throws IOException;
1033 
1034         void add(LdapFilter filter);
1035 
1036         boolean isFullSearch();
1037 
1038         boolean isMatch(ExchangeSession.Contact person);
1039 
1040         /**
1041          * Collect distinct non-wildcard search values from this filter tree.
1042          *
1043          * @param values set to collect values into
1044          */
1045         void collectSearchValues(Set<String> values);
1046     }
1047 
1048     class CompoundFilter implements LdapFilter {
1049         final Set<LdapFilter> criteria = new HashSet<>();
1050         final int type;
1051 
1052         CompoundFilter(int filterType) {
1053             type = filterType;
1054         }
1055 
1056         @Override
1057         public String toString() {
1058             StringBuilder buffer = new StringBuilder();
1059 
1060             if (type == LDAP_FILTER_OR) {
1061                 buffer.append("(|");
1062             } else if (type == LDAP_FILTER_AND) {
1063                 buffer.append("(&");
1064             } else {
1065                 buffer.append("(!");
1066             }
1067 
1068             for (LdapFilter child : criteria) {
1069                 buffer.append(child.toString());
1070             }
1071 
1072             buffer.append(')');
1073 
1074             return buffer.toString();
1075         }
1076 
1077         /**
1078          * Add child filter
1079          *
1080          * @param filter inner filter
1081          */
1082         public void add(LdapFilter filter) {
1083             criteria.add(filter);
1084         }
1085 
1086         /**
1087          * This is only a full search if every child
1088          * is also a full search
1089          *
1090          * @return true if full search filter
1091          */
1092         public boolean isFullSearch() {
1093             for (LdapFilter child : criteria) {
1094                 if (!child.isFullSearch()) {
1095                     return false;
1096                 }
1097             }
1098 
1099             return true;
1100         }
1101 
1102         /**
1103          * Build search filter for Contacts folder search.
1104          * Use Exchange SEARCH syntax
1105          *
1106          * @return contact search filter
1107          */
1108         public ExchangeSession.Condition getContactSearchFilter() {
1109             ExchangeSession.MultiCondition condition;
1110 
1111             if (type == LDAP_FILTER_OR) {
1112                 condition = session.or();
1113             } else {
1114                 condition = session.and();
1115             }
1116 
1117             for (LdapFilter child : criteria) {
1118                 condition.add(child.getContactSearchFilter());
1119             }
1120 
1121             return condition;
1122         }
1123 
1124         /**
1125          * Test if person matches the current filter.
1126          *
1127          * @param person person attributes map
1128          * @return true if filter match
1129          */
1130         public boolean isMatch(ExchangeSession.Contact person) {
1131             if (type == LDAP_FILTER_OR) {
1132                 for (LdapFilter child : criteria) {
1133                     if (!child.isFullSearch()) {
1134                         if (child.isMatch(person)) {
1135                             // We've found a match
1136                             return true;
1137                         }
1138                     }
1139                 }
1140 
1141                 // No subconditions are met
1142                 return false;
1143             } else if (type == LDAP_FILTER_AND) {
1144                 for (LdapFilter child : criteria) {
1145                     if (!child.isFullSearch()) {
1146                         if (!child.isMatch(person)) {
1147                             // We've found a miss
1148                             return false;
1149                         }
1150                     }
1151                 }
1152 
1153                 // All subconditions are met
1154                 return true;
1155             }
1156 
1157             return false;
1158         }
1159 
1160         /**
1161          * Find persons in Exchange GAL matching filter.
1162          * Iterate over child filters to build results.
1163          *
1164          * @param session Exchange session
1165          * @return persons map
1166          * @throws IOException on error
1167          */
1168         public Map<String, ExchangeSession.Contact> findInGAL(ExchangeSession session, Set<String> returningAttributes, int sizeLimit) throws IOException {
1169             Map<String, ExchangeSession.Contact> persons = null;
1170 
1171             if (type == LDAP_FILTER_AND && session.supportsPeopleSearch()) {
1172                 // People API supports multi-word search natively.
1173                 // Extract distinct search values from child OR groups and issue a single query
1174                 // instead of N*M redundant identical API calls.
1175                 Set<String> searchTerms = new LinkedHashSet<>();
1176                 collectSearchValues(searchTerms);
1177                 if (!searchTerms.isEmpty()) {
1178                     String combinedSearch = String.join(" ", searchTerms);
1179                     Map<String, ExchangeSession.Contact> galPersons = session.galFind(
1180                             session.startsWith("cn", combinedSearch),
1181                             convertLdapToContactReturningAttributes(returningAttributes), sizeLimit);
1182                     // post-filter against the full compound filter
1183                     persons = new HashMap<>();
1184                     for (ExchangeSession.Contact person : galPersons.values()) {
1185                         if (isMatch(person)) {
1186                             persons.put(person.get("uid"), person);
1187                         }
1188                     }
1189                     return persons;
1190                 }
1191             }
1192 
1193             for (LdapFilter child : criteria) {
1194                 int currentSizeLimit = sizeLimit;
1195                 if (persons != null) {
1196                     currentSizeLimit -= persons.size();
1197                 }
1198                 Map<String, ExchangeSession.Contact> childFind = child.findInGAL(session, returningAttributes, currentSizeLimit);
1199 
1200                 if (childFind != null) {
1201                     if (persons == null) {
1202                         persons = childFind;
1203                     } else if (type == LDAP_FILTER_OR) {
1204                         // Create the union of the existing results and the child found results
1205                         persons.putAll(childFind);
1206                     } else if (type == LDAP_FILTER_AND) {
1207                         // Append current child filter results that match all child filters to persons.
1208                         // The hard part is that, due to the 100-item-returned galFind limit
1209                         // we may catch new items that match all child filters in each child search.
1210                         // Thus, instead of building the intersection, we check each result against
1211                         // all filters.
1212 
1213                         for (ExchangeSession.Contact result : childFind.values()) {
1214                             if (isMatch(result)) {
1215                                 // This item from the child result set matches all sub-criteria, add it
1216                                 persons.put(result.get("uid"), result);
1217                             }
1218                         }
1219                     }
1220                 }
1221             }
1222 
1223             if ((persons == null) && !isFullSearch()) {
1224                 // return an empty map (indicating no results were found)
1225                 return new HashMap<>();
1226             }
1227 
1228             return persons;
1229         }
1230 
1231         public void collectSearchValues(Set<String> values) {
1232             for (LdapFilter child : criteria) {
1233                 child.collectSearchValues(values);
1234             }
1235         }
1236     }
1237 
1238     class SimpleFilter implements LdapFilter {
1239         static final String STAR = "*";
1240         final String attributeName;
1241         final String value;
1242         final int mode;
1243         final int operator;
1244         final boolean canIgnore;
1245 
1246         SimpleFilter(String attributeName) {
1247             this.attributeName = attributeName;
1248             this.value = SimpleFilter.STAR;
1249             this.operator = LDAP_FILTER_SUBSTRINGS;
1250             this.mode = 0;
1251             this.canIgnore = checkIgnore();
1252         }
1253 
1254         SimpleFilter(String attributeName, String value, int ldapFilterOperator, int ldapFilterMode) {
1255             this.attributeName = attributeName;
1256             this.value = value;
1257             this.operator = ldapFilterOperator;
1258             this.mode = ldapFilterMode;
1259             this.canIgnore = checkIgnore();
1260         }
1261 
1262         private boolean checkIgnore() {
1263             if ("objectclass".equals(attributeName) && STAR.equals(value)) {
1264                 // ignore cases where any object class can match
1265                 return true;
1266             } else if (IGNORE_MAP.contains(attributeName)) {
1267                 // Ignore this specific attribute
1268                 return true;
1269             } else if (CRITERIA_MAP.get(attributeName) == null && getContactAttributeName(attributeName) == null) {
1270                 DavGatewayTray.debug(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER_ATTRIBUTE",
1271                         attributeName, value));
1272 
1273                 return true;
1274             }
1275 
1276             return false;
1277         }
1278 
1279         public boolean isFullSearch() {
1280             // only (objectclass=*) is a full search
1281             return "objectclass".equals(attributeName) && STAR.equals(value);
1282         }
1283 
1284         @Override
1285         public String toString() {
1286             StringBuilder buffer = new StringBuilder();
1287             buffer.append('(');
1288             buffer.append(attributeName);
1289             buffer.append('=');
1290             if (SimpleFilter.STAR.equals(value)) {
1291                 buffer.append(SimpleFilter.STAR);
1292             } else if (operator == LDAP_FILTER_SUBSTRINGS) {
1293                 if (mode == LDAP_SUBSTRING_FINAL || mode == LDAP_SUBSTRING_ANY) {
1294                     buffer.append(SimpleFilter.STAR);
1295                 }
1296                 buffer.append(value);
1297                 if (mode == LDAP_SUBSTRING_INITIAL || mode == LDAP_SUBSTRING_ANY) {
1298                     buffer.append(SimpleFilter.STAR);
1299                 }
1300             } else {
1301                 buffer.append(value);
1302             }
1303 
1304             buffer.append(')');
1305             return buffer.toString();
1306         }
1307 
1308         public ExchangeSession.Condition getContactSearchFilter() {
1309             String contactAttributeName = getContactAttributeName(attributeName);
1310 
1311             if (canIgnore || (contactAttributeName == null)) {
1312                 return null;
1313             }
1314 
1315             ExchangeSession.Condition condition = null;
1316 
1317             if (operator == LDAP_FILTER_EQUALITY) {
1318                 condition = session.isEqualTo(contactAttributeName, value);
1319             } else if ("*".equals(value)) {
1320                 condition = session.not(session.isNull(contactAttributeName));
1321                 // do not allow substring search on integer field imapUid
1322             } else if (!"imapUid".equals(contactAttributeName)) {
1323                 // endsWith not supported by exchange, convert to contains
1324                 if (mode == LDAP_SUBSTRING_FINAL || mode == LDAP_SUBSTRING_ANY) {
1325                     condition = session.contains(contactAttributeName, value);
1326                 } else {
1327                     condition = session.startsWith(contactAttributeName, value);
1328                 }
1329             }
1330             return condition;
1331         }
1332 
1333         public boolean isMatch(ExchangeSession.Contact person) {
1334             if (canIgnore) {
1335                 // Ignore this filter
1336                 return true;
1337             }
1338             String contactAttributeName = getContactAttributeName(attributeName);
1339 
1340             String personAttributeValue = person.get(contactAttributeName);
1341 
1342             if (personAttributeValue == null) {
1343                 // No value to allow for filter match
1344                 return false;
1345             } else if (value == null) {
1346                 // This is a presence filter: found
1347                 return true;
1348             } else if ((operator == LDAP_FILTER_EQUALITY) && personAttributeValue.equalsIgnoreCase(value)) {
1349                 // Found an exact match
1350                 return true;
1351             } else //noinspection RedundantIfStatement
1352                 if ((operator == LDAP_FILTER_SUBSTRINGS) && (personAttributeValue.toLowerCase().contains(value.toLowerCase()))) {
1353                     // Found a substring match
1354                     return true;
1355                 }
1356 
1357             return false;
1358         }
1359 
1360         public Map<String, ExchangeSession.Contact> findInGAL(ExchangeSession session, Set<String> returningAttributes, int sizeLimit) throws IOException {
1361             if (canIgnore) {
1362                 return null;
1363             }
1364 
1365             String contactAttributeName = getContactAttributeName(attributeName);
1366 
1367             if (contactAttributeName != null) {
1368                 // for wildcard searches, use empty string on backends with native people search,
1369                 // fall back to "A" prefix on legacy backends (old OWA galfind workaround)
1370                 String searchValue = value;
1371                 if ("*".equals(value)) {
1372                     searchValue = session.supportsPeopleSearch() ? "" : "A";
1373                 }
1374                 Map<String, ExchangeSession.Contact> galPersons = session.galFind(session.startsWith(contactAttributeName, searchValue),
1375                         convertLdapToContactReturningAttributes(returningAttributes), sizeLimit);
1376 
1377                 if (operator == LDAP_FILTER_EQUALITY) {
1378                     // Make sure only exact matches are returned
1379 
1380                     Map<String, ExchangeSession.Contact> results = new HashMap<>();
1381 
1382                     for (ExchangeSession.Contact person : galPersons.values()) {
1383                         if (isMatch(person)) {
1384                             // Found an exact match
1385                             results.put(person.get("uid"), person);
1386                         }
1387                     }
1388 
1389                     return results;
1390                 } else if (session.supportsPeopleSearch()) {
1391                     // People API returns fuzzy/relevance-based results,
1392                     // post-filter to ensure they match the original LDAP filter
1393                     Map<String, ExchangeSession.Contact> results = new HashMap<>();
1394 
1395                     for (ExchangeSession.Contact person : galPersons.values()) {
1396                         if (isMatch(person)) {
1397                             results.put(person.get("uid"), person);
1398                         }
1399                     }
1400 
1401                     return results;
1402                 } else {
1403                     return galPersons;
1404                 }
1405             }
1406 
1407             return null;
1408         }
1409 
1410         public void add(LdapFilter filter) {
1411             // Should never be called
1412             DavGatewayTray.error(new BundleMessage("LOG_LDAP_UNSUPPORTED_FILTER", "nested simple filters"));
1413         }
1414 
1415         public void collectSearchValues(Set<String> values) {
1416             if (!canIgnore && value != null && !STAR.equals(value)) {
1417                 values.add(value);
1418             }
1419         }
1420     }
1421 
1422     /**
1423      * Convert contact attribute name to LDAP attribute name.
1424      *
1425      * @param ldapAttributeName ldap attribute name
1426      * @return contact attribute name
1427      */
1428     protected static String getContactAttributeName(String ldapAttributeName) {
1429         String contactAttributeName = null;
1430         // first look in ldap to contact attribute map
1431         if (LDAP_TO_CONTACT_ATTRIBUTE_MAP.containsKey(ldapAttributeName)) {
1432             String mappedAttribute = LDAP_TO_CONTACT_ATTRIBUTE_MAP.get(ldapAttributeName);
1433             if (mappedAttribute != null) {
1434                 contactAttributeName = mappedAttribute;
1435             }
1436         // then map contact attributes
1437         } else if (ExchangeSession.CONTACT_ATTRIBUTES.contains(ldapAttributeName)) {
1438             contactAttributeName = ldapAttributeName;
1439         } else if (!"hassubordinates".equals(ldapAttributeName)){
1440             DavGatewayTray.debug(new BundleMessage("UNKNOWN_ATTRIBUTE", ldapAttributeName));
1441         }
1442         return contactAttributeName;
1443     }
1444 
1445     /**
1446      * Convert LDAP attribute name to contact attribute name.
1447      *
1448      * @param contactAttributeName ldap attribute name
1449      * @return contact attribute name
1450      */
1451     protected static String getLdapAttributeName(String contactAttributeName) {
1452         String mappedAttributeName = CONTACT_TO_LDAP_ATTRIBUTE_MAP.get(contactAttributeName);
1453         if (mappedAttributeName != null) {
1454             return mappedAttributeName;
1455         } else {
1456             return contactAttributeName;
1457         }
1458     }
1459 
1460     protected Set<String> convertLdapToContactReturningAttributes(Set<String> returningAttributes) {
1461         Set<String> contactReturningAttributes;
1462         if (returningAttributes != null && !returningAttributes.isEmpty()) {
1463             contactReturningAttributes = new HashSet<>();
1464             // always return uid
1465             contactReturningAttributes.add("imapUid");
1466             for (String attribute : returningAttributes) {
1467                 String contactAttributeName = getContactAttributeName(attribute);
1468                 if (contactAttributeName != null) {
1469                     contactReturningAttributes.add(contactAttributeName);
1470                 }
1471             }
1472         } else {
1473             contactReturningAttributes = ExchangeSession.CONTACT_ATTRIBUTES;
1474         }
1475         return contactReturningAttributes;
1476     }
1477 
1478     protected class SearchRunnable implements Runnable {
1479         private final int currentMessageId;
1480         private final String dn;
1481         private final int scope;
1482         private final int sizeLimit;
1483         private final int timelimit;
1484         private final LdapFilter ldapFilter;
1485         private final Set<String> returningAttributes;
1486         private boolean abandon;
1487 
1488         protected SearchRunnable(int currentMessageId, String dn, int scope, int sizeLimit, int timelimit, LdapFilter ldapFilter, Set<String> returningAttributes) {
1489             this.currentMessageId = currentMessageId;
1490             this.dn = dn;
1491             this.scope = scope;
1492             this.sizeLimit = sizeLimit;
1493             this.timelimit = timelimit;
1494             this.ldapFilter = ldapFilter;
1495             this.returningAttributes = returningAttributes;
1496         }
1497 
1498         /**
1499          * Abandon search.
1500          */
1501         protected void abandon() {
1502             abandon = true;
1503         }
1504 
1505         public void run() {
1506             try {
1507                 int size = 0;
1508                 DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH", currentMessageId, dn, scope, sizeLimit, timelimit, ldapFilter.toString(), returningAttributes));
1509 
1510                 if (scope == SCOPE_BASE_OBJECT) {
1511                     if (dn != null && dn.isEmpty()) {
1512                         size = 1;
1513                         sendRootDSE(currentMessageId);
1514                     } else if (BASE_CONTEXT.equals(dn)) {
1515                         size = 1;
1516                         // root
1517                         sendBaseContext(currentMessageId);
1518                     } else if (dn != null && dn.startsWith("uid=") && dn.indexOf(',') > 0) {
1519                         if (session != null) {
1520                             // single user request
1521                             String uid = dn.substring("uid=".length(), dn.indexOf(','));
1522                             Map<String, ExchangeSession.Contact> persons = null;
1523 
1524                             // first search in contact
1525                             try {
1526                                 // check if this is a contact uid
1527                                 Integer.parseInt(uid);
1528                                 persons = contactFind(session.isEqualTo("imapUid", uid), returningAttributes, sizeLimit);
1529                             } catch (NumberFormatException e) {
1530                                 // ignore, this is not a contact uid
1531                             }
1532 
1533                             // then in GAL
1534                             if (persons == null || persons.isEmpty()) {
1535                                 persons = session.galFind(session.isEqualTo("imapUid", uid),
1536                                         convertLdapToContactReturningAttributes(returningAttributes), sizeLimit);
1537 
1538                                 ExchangeSession.Contact person = persons.get(uid.toLowerCase());
1539                                 // filter out non exact results
1540                                 if (persons.size() > 1 || person == null) {
1541                                     persons = new HashMap<>();
1542                                     if (person != null) {
1543                                         persons.put(uid.toLowerCase(), person);
1544                                     }
1545                                 }
1546                             }
1547                             size = persons.size();
1548                             sendPersons(currentMessageId, dn.substring(dn.indexOf(',')), persons, returningAttributes);
1549                         } else {
1550                             DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_ANONYMOUS_ACCESS_FORBIDDEN", currentMessageId, dn));
1551                         }
1552                     } else {
1553                         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_INVALID_DN", currentMessageId, dn));
1554                     }
1555                 } else if (COMPUTER_CONTEXT.equals(dn) || COMPUTER_CONTEXT_LION.equals(dn)) {
1556                     size = 1;
1557                     // computer context for iCal
1558                     sendComputerContext(currentMessageId, returningAttributes);
1559                 } else if ((BASE_CONTEXT.equalsIgnoreCase(dn) || OD_USER_CONTEXT.equalsIgnoreCase(dn)) || OD_USER_CONTEXT_LION.equalsIgnoreCase(dn)) {
1560                     if (session != null) {
1561                         Map<String, ExchangeSession.Contact> persons = new HashMap<>();
1562                         if (ldapFilter.isFullSearch()) {
1563                             // append personal contacts first
1564                             for (ExchangeSession.Contact person : contactFind(null, returningAttributes, sizeLimit).values()) {
1565                                 persons.put(person.get("imapUid"), person);
1566                                 if (persons.size() == sizeLimit) {
1567                                     break;
1568                                 }
1569                             }
1570                             // full search
1571                             if (session.supportsPeopleSearch()) {
1572                                 // backend supports native broad search (e.g. Graph People API), single call
1573                                 if (!abandon && persons.size() < sizeLimit) {
1574                                     for (ExchangeSession.Contact person : session.galFind(session.startsWith("cn", ""),
1575                                             convertLdapToContactReturningAttributes(returningAttributes), sizeLimit).values()) {
1576                                         persons.put(person.get("uid"), person);
1577                                         if (persons.size() == sizeLimit) {
1578                                             break;
1579                                         }
1580                                     }
1581                                 }
1582                             } else {
1583                                 // legacy backends: iterate A-Z to retrieve GAL entries
1584                                 for (char c = 'A'; c <= 'Z'; c++) {
1585                                     if (!abandon && persons.size() < sizeLimit) {
1586                                         for (ExchangeSession.Contact person : session.galFind(session.startsWith("cn", String.valueOf(c)),
1587                                                 convertLdapToContactReturningAttributes(returningAttributes), sizeLimit).values()) {
1588                                             persons.put(person.get("uid"), person);
1589                                             if (persons.size() == sizeLimit) {
1590                                                 break;
1591                                             }
1592                                         }
1593                                     }
1594                                     if (persons.size() == sizeLimit) {
1595                                         break;
1596                                     }
1597                                 }
1598                             }
1599                         } else {
1600                             // append personal contacts first
1601                             ExchangeSession.Condition filter = ldapFilter.getContactSearchFilter();
1602 
1603                             // if ldapfilter is not a full search and filter is null,
1604                             // ignored all attribute filters => return empty results
1605                             if (ldapFilter.isFullSearch() || filter != null) {
1606                                 for (ExchangeSession.Contact person : contactFind(filter, returningAttributes, sizeLimit).values()) {
1607                                     persons.put(person.get("imapUid"), person);
1608 
1609                                     if (persons.size() == sizeLimit) {
1610                                         break;
1611                                     }
1612                                 }
1613                                 if (!abandon && persons.size() < sizeLimit) {
1614                                     for (ExchangeSession.Contact person : ldapFilter.findInGAL(session, returningAttributes, sizeLimit - persons.size()).values()) {
1615                                         if (persons.size() == sizeLimit) {
1616                                             break;
1617                                         }
1618 
1619                                         persons.put(person.get("uid"), person);
1620                                     }
1621                                 }
1622                             }
1623                         }
1624 
1625                         size = persons.size();
1626                         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_FOUND_RESULTS", currentMessageId, size));
1627                         sendPersons(currentMessageId, ", " + dn, persons, returningAttributes);
1628                         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_END", currentMessageId));
1629                     } else {
1630                         DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_ANONYMOUS_ACCESS_FORBIDDEN", currentMessageId, dn));
1631                     }
1632                 } else if (dn != null && !dn.isEmpty() && !OD_CONFIG_CONTEXT.equals(dn) && !OD_GROUP_CONTEXT.equals(dn)) {
1633                     DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_INVALID_DN", currentMessageId, dn));
1634                 }
1635 
1636                 // iCal: do not send LDAP_SIZE_LIMIT_EXCEEDED on apple-computer search by cn with sizelimit 1
1637                 if (size > 1 && size == sizeLimit) {
1638                     DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_SIZE_LIMIT_EXCEEDED", currentMessageId));
1639                     sendClient(currentMessageId, LDAP_REP_RESULT, LDAP_SIZE_LIMIT_EXCEEDED, "");
1640                 } else {
1641                     DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_SUCCESS", currentMessageId));
1642                     sendClient(currentMessageId, LDAP_REP_RESULT, LDAP_SUCCESS, "");
1643                 }
1644             } catch (SocketException e) {
1645                 // client closed connection
1646                 LOGGER.warn(BundleMessage.formatLog("LOG_CLIENT_CLOSED_CONNECTION"));
1647             } catch (IOException e) {
1648                 DavGatewayTray.log(e);
1649                 try {
1650                     sendErr(currentMessageId, LDAP_REP_RESULT, e);
1651                 } catch (IOException e2) {
1652                     DavGatewayTray.debug(new BundleMessage("LOG_EXCEPTION_SENDING_ERROR_TO_CLIENT"), e2);
1653                 }
1654             } finally {
1655                 synchronized (searchThreadMap) {
1656                     searchThreadMap.remove(currentMessageId);
1657                 }
1658             }
1659 
1660             DavGatewayTray.resetIcon();
1661         }
1662 
1663         /**
1664          * Search users in contacts folder
1665          *
1666          * @param condition           search filter
1667          * @param returningAttributes requested attributes
1668          * @param maxCount            maximum item count
1669          * @return Contact map
1670          * @throws IOException on error
1671          */
1672         public Map<String, ExchangeSession.Contact> contactFind(ExchangeSession.Condition condition, Set<String> returningAttributes, int maxCount) throws IOException {
1673             Map<String, ExchangeSession.Contact> results = new HashMap<>();
1674 
1675             Set<String> contactReturningAttributes = convertLdapToContactReturningAttributes(returningAttributes);
1676             contactReturningAttributes.remove("apple-serviceslocator");
1677             List<ExchangeSession.Contact> contacts = session.searchContacts(ExchangeSession.CONTACTS, contactReturningAttributes, condition, maxCount);
1678 
1679             for (ExchangeSession.Contact contact : contacts) {
1680                 // use imapUid as uid
1681                 String imapUid = contact.get("imapUid");
1682                 if (imapUid != null) {
1683                     results.put(imapUid, contact);
1684                 }
1685             }
1686 
1687             return results;
1688         }
1689 
1690 
1691         /**
1692          * Convert to LDAP attributes and send entry
1693          *
1694          * @param currentMessageId    current Message Id
1695          * @param baseContext         request base context (BASE_CONTEXT or OD_BASE_CONTEXT)
1696          * @param persons             persons Map
1697          * @param returningAttributes returning attributes
1698          * @throws IOException on error
1699          */
1700         protected void sendPersons(int currentMessageId, String baseContext, Map<String, ExchangeSession.Contact> persons, Set<String> returningAttributes) throws IOException {
1701             boolean needObjectClasses = returningAttributes.contains("objectclass") || returningAttributes.isEmpty();
1702             boolean returnAllAttributes = returningAttributes.isEmpty();
1703 
1704             for (ExchangeSession.Contact person : persons.values()) {
1705                 if (abandon) {
1706                     break;
1707                 }
1708 
1709                 Map<String, Object> ldapPerson = new HashMap<>();
1710 
1711                 // convert Contact entries
1712                 if (returnAllAttributes) {
1713                     // just convert contact attributes to default ldap names
1714                     for (Map.Entry<String, String> entry : person.entrySet()) {
1715                         String ldapAttribute = getLdapAttributeName(entry.getKey());
1716                         String value = entry.getValue();
1717                         if (value != null) {
1718                             ldapPerson.put(ldapAttribute, value);
1719                         }
1720                     }
1721                 } else {
1722                     // always map uid
1723                     ldapPerson.put("uid", person.get("imapUid"));
1724                     // iterate over requested attributes
1725                     for (String ldapAttribute : returningAttributes) {
1726                         String contactAttribute = getContactAttributeName(ldapAttribute);
1727                         String value = person.get(contactAttribute);
1728                         if (value != null) {
1729                             if (ldapAttribute.startsWith("birth")) {
1730                                 SimpleDateFormat parser = ExchangeSession.getZuluDateFormat();
1731                                 Calendar calendar = Calendar.getInstance();
1732                                 try {
1733                                     calendar.setTime(parser.parse(value));
1734                                 } catch (ParseException e) {
1735                                     throw new IOException(e + " " + e.getMessage());
1736                                 }
1737                                 switch (ldapAttribute) {
1738                                     case "birthday":
1739                                         value = String.valueOf(calendar.get(Calendar.DAY_OF_MONTH));
1740                                         break;
1741                                     case "birthmonth":
1742                                         value = String.valueOf(calendar.get(Calendar.MONTH) + 1);
1743                                         break;
1744                                     case "birthyear":
1745                                         value = String.valueOf(calendar.get(Calendar.YEAR));
1746                                         break;
1747                                 }
1748                             }
1749                             ldapPerson.put(ldapAttribute, value);
1750                         } else if ("hassubordinates".equals(ldapAttribute)) {
1751                             ldapPerson.put(ldapAttribute, "false");
1752                         }
1753                     }
1754                 }
1755 
1756                 // Process all attributes which have static mappings
1757                 for (Map.Entry<String, String> entry : STATIC_ATTRIBUTE_MAP.entrySet()) {
1758                     String ldapAttribute = entry.getKey();
1759                     String value = entry.getValue();
1760 
1761                     if (value != null
1762                             && (returnAllAttributes || returningAttributes.contains(ldapAttribute))) {
1763                         ldapPerson.put(ldapAttribute, value);
1764                     }
1765                 }
1766 
1767                 if (needObjectClasses) {
1768                     ldapPerson.put("objectClass", PERSON_OBJECT_CLASSES);
1769                 }
1770 
1771                 // iCal: copy email to apple-generateduid, encode @
1772                 if (returnAllAttributes || returningAttributes.contains("apple-generateduid")) {
1773                     String mail = (String) ldapPerson.get("mail");
1774                     if (mail != null) {
1775                         ldapPerson.put("apple-generateduid", mail.replaceAll("@", "__AT__"));
1776                     } else {
1777                         // failover, should not happen
1778                         ldapPerson.put("apple-generateduid", ldapPerson.get("uid"));
1779                     }
1780                 }
1781                 if (ldapPerson.containsKey("msexchangecertificate;binary")) {
1782                     String certificate = (String) ldapPerson.get("msexchangecertificate;binary");
1783                     ldapPerson.put("msexchangecertificate;binary", IOUtil.decodeBase64(certificate));
1784                 }
1785                 if (ldapPerson.containsKey("usersmimecertificate;binary")) {
1786                     String certificate = (String) ldapPerson.get("usersmimecertificate;binary");
1787                     ldapPerson.put("usersmimecertificate;binary", IOUtil.decodeBase64(certificate));
1788                 }
1789 
1790                 // iCal: replace current user alias with login name
1791                 if (session.getAlias().equals(ldapPerson.get("uid"))) {
1792                     if (returningAttributes.contains("uidnumber")) {
1793                         ldapPerson.put("uidnumber", userName);
1794                     }
1795                 }
1796                 DavGatewayTray.debug(new BundleMessage("LOG_LDAP_REQ_SEARCH_SEND_PERSON", currentMessageId, ldapPerson.get("uid"), baseContext, ldapPerson));
1797 
1798                 try {
1799                     sendEntry(currentMessageId, new Rdn("uid", ldapPerson.get("uid")) + baseContext, ldapPerson);
1800                 } catch (InvalidNameException e) {
1801                     throw new IOException(e);
1802                 }
1803             }
1804 
1805         }
1806 
1807     }
1808 }