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