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