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