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