libtdepim

ldapclient.cpp
1 /* kldapclient.cpp - LDAP access
2  * Copyright (C) 2002 Klarälvdalens Datakonsult AB
3  *
4  * Author: Steffen Hansen <hansen@kde.org>
5  *
6  * Ported to KABC by Daniel Molkentin <molkentin@kde.org>
7  *
8  * This file is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This file is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21  */
22 
23 
24 
25 #include <tqfile.h>
26 #include <tqimage.h>
27 #include <tqlabel.h>
28 #include <tqpixmap.h>
29 #include <tqtextstream.h>
30 #include <tqurl.h>
31 
32 #include <tdeabc/ldapurl.h>
33 #include <tdeabc/ldif.h>
34 #include <tdeapplication.h>
35 #include <tdeconfig.h>
36 #include <kdebug.h>
37 #include <kdirwatch.h>
38 #include <kmdcodec.h>
39 #include <kprotocolinfo.h>
40 #include <kstandarddirs.h>
41 #include <kstaticdeleter.h>
42 
43 #include "ldapclient.h"
44 
45 using namespace KPIM;
46 
47 TDEConfig *KPIM::LdapSearch::s_config = 0L;
48 static KStaticDeleter<TDEConfig> configDeleter;
49 
50 TQString LdapObject::toString() const
51 {
52  TQString result = TQString::fromLatin1( "\ndn: %1\n" ).arg( dn );
53  for ( LdapAttrMap::ConstIterator it = attrs.begin(); it != attrs.end(); ++it ) {
54  TQString attr = it.key();
55  for ( LdapAttrValue::ConstIterator it2 = (*it).begin(); it2 != (*it).end(); ++it2 ) {
56  result += TQString::fromUtf8( TDEABC::LDIF::assembleLine( attr, *it2, 76 ) ) + "\n";
57  }
58  }
59 
60  return result;
61 }
62 
63 void LdapObject::clear()
64 {
65  dn = TQString();
66  objectClass = TQString();
67  attrs.clear();
68 }
69 
70 void LdapObject::assign( const LdapObject& that )
71 {
72  if ( &that != this ) {
73  dn = that.dn;
74  attrs = that.attrs;
75  client = that.client;
76  }
77 }
78 
79 LdapClient::LdapClient( int clientNumber, TQObject* parent, const char* name )
80  : TQObject( parent, name ), mJob( 0 ), mActive( false ), mReportObjectClass( false )
81 {
82 // d = new LdapClientPrivate;
83  mClientNumber = clientNumber;
84  mCompletionWeight = 50 - mClientNumber;
85 }
86 
87 LdapClient::~LdapClient()
88 {
89  cancelQuery();
90 // delete d; d = 0;
91 }
92 
93 void LdapClient::setAttrs( const TQStringList& attrs )
94 {
95  mAttrs = attrs;
96  for ( TQStringList::Iterator it = mAttrs.begin(); it != mAttrs.end(); ++it )
97  if( (*it).lower() == "objectclass" ){
98  mReportObjectClass = true;
99  return;
100  }
101  mAttrs << "objectClass"; // via objectClass we detect distribution lists
102  mReportObjectClass = false;
103 }
104 
105 void LdapClient::startQuery( const TQString& filter )
106 {
107  cancelQuery();
108  TDEABC::LDAPUrl url;
109 
110  url.setProtocol( ( mServer.security() == LdapServer::SSL ) ? "ldaps" : "ldap" );
111  if ( mServer.auth() != LdapServer::Anonymous ) {
112  url.setUser( mServer.user() );
113  url.setPass( mServer.pwdBindDN() );
114  }
115  url.setHost( mServer.host() );
116  url.setPort( mServer.port() );
117  url.setExtension( "x-ver", TQString::number( mServer.version() ) );
118  url.setDn( mServer.baseDN() );
119  url.setDn( mServer.baseDN() );
120  if ( mServer.security() == LdapServer::TLS ) url.setExtension( "x-tls","" );
121  if ( mServer.auth() == LdapServer::SASL ) {
122  url.setExtension( "x-sasl","" );
123  if ( !mServer.bindDN().isEmpty() ) url.setExtension( "x-bindname", mServer.bindDN() );
124  if ( !mServer.mech().isEmpty() ) url.setExtension( "x-mech", mServer.mech() );
125  }
126  if ( mServer.timeLimit() != 0 ) url.setExtension( "x-timelimit",
127  TQString::number( mServer.timeLimit() ) );
128  if ( mServer.sizeLimit() != 0 ) url.setExtension( "x-sizelimit",
129  TQString::number( mServer.sizeLimit() ) );
130 
131  url.setAttributes( mAttrs );
132  url.setScope( mScope == "one" ? TDEABC::LDAPUrl::One : TDEABC::LDAPUrl::Sub );
133  url.setFilter( "("+filter+")" );
134 
135  kdDebug(5300) << "LdapClient: Doing query: " << url.prettyURL() << endl;
136 
137  startParseLDIF();
138  mActive = true;
139  mJob = TDEIO::get( url, false, false );
140  connect( mJob, TQ_SIGNAL( data( TDEIO::Job*, const TQByteArray& ) ),
141  this, TQ_SLOT( slotData( TDEIO::Job*, const TQByteArray& ) ) );
142  connect( mJob, TQ_SIGNAL( infoMessage( TDEIO::Job*, const TQString& ) ),
143  this, TQ_SLOT( slotInfoMessage( TDEIO::Job*, const TQString& ) ) );
144  connect( mJob, TQ_SIGNAL( result( TDEIO::Job* ) ),
145  this, TQ_SLOT( slotDone() ) );
146 }
147 
149 {
150  if ( mJob ) {
151  mJob->kill();
152  mJob = 0;
153  }
154 
155  mActive = false;
156 }
157 
158 void LdapClient::slotData( TDEIO::Job*, const TQByteArray& data )
159 {
160  parseLDIF( data );
161 }
162 
163 void LdapClient::slotInfoMessage( TDEIO::Job*, const TQString & )
164 {
165  //tqDebug("Job said \"%s\"", info.latin1());
166 }
167 
168 void LdapClient::slotDone()
169 {
170  endParseLDIF();
171  mActive = false;
172 #if 0
173  for ( TQValueList<LdapObject>::Iterator it = mObjects.begin(); it != mObjects.end(); ++it ) {
174  tqDebug( (*it).toString().latin1() );
175  }
176 #endif
177  int err = mJob->error();
178  if ( err && err != TDEIO::ERR_USER_CANCELED ) {
179  emit error( mJob->errorString() );
180  }
181  emit done();
182 }
183 
184 void LdapClient::startParseLDIF()
185 {
186  mCurrentObject.clear();
187  mLdif.startParsing();
188 }
189 
190 void LdapClient::endParseLDIF()
191 {
192 }
193 
194 void LdapClient::finishCurrentObject()
195 {
196  mCurrentObject.dn = mLdif.dn();
197  const TQString sClass( mCurrentObject.objectClass.lower() );
198  if( sClass == "groupofnames" || sClass == "kolabgroupofnames" ){
199  LdapAttrMap::ConstIterator it = mCurrentObject.attrs.find("mail");
200  if( it == mCurrentObject.attrs.end() ){
201  // No explicit mail address found so far?
202  // Fine, then we use the address stored in the DN.
203  TQString sMail;
204  TQStringList lMail = TQStringList::split(",dc=", mCurrentObject.dn);
205  const int n = lMail.count();
206  if( n ){
207  if( lMail.first().lower().startsWith("cn=") ){
208  sMail = lMail.first().simplifyWhiteSpace().mid(3);
209  if( 1 < n )
210  sMail.append('@');
211  for( int i=1; i<n; ++i){
212  sMail.append( lMail[i] );
213  if( i < n-1 )
214  sMail.append('.');
215  }
216  mCurrentObject.attrs["mail"].append( sMail.utf8() );
217  }
218  }
219  }
220  }
221  mCurrentObject.client = this;
222  emit result( mCurrentObject );
223  mCurrentObject.clear();
224 }
225 
226 void LdapClient::parseLDIF( const TQByteArray& data )
227 {
228  //kdDebug(5300) << "LdapClient::parseLDIF( " << TQCString(data.data(), data.size()+1) << " )" << endl;
229  if ( data.size() ) {
230  mLdif.setLDIF( data );
231  } else {
232  mLdif.endLDIF();
233  }
234 
235  TDEABC::LDIF::ParseVal ret;
236  TQString name;
237  do {
238  ret = mLdif.nextItem();
239  switch ( ret ) {
240  case TDEABC::LDIF::Item:
241  {
242  name = mLdif.attr();
243  // Must make a copy! TQByteArray is explicitely shared
244  TQByteArray value = mLdif.val().copy();
245  bool bIsObjectClass = name.lower() == "objectclass";
246  if( bIsObjectClass )
247  mCurrentObject.objectClass = TQString::fromUtf8( value, value.size() );
248  if( mReportObjectClass || !bIsObjectClass )
249  mCurrentObject.attrs[ name ].append( value );
250  //kdDebug(5300) << "LdapClient::parseLDIF(): name=" << name << " value=" << TQCString(value.data(), value.size()+1) << endl;
251  }
252  break;
253  case TDEABC::LDIF::EndEntry:
254  finishCurrentObject();
255  break;
256  default:
257  break;
258  }
259  } while ( ret != TDEABC::LDIF::MoreData );
260 }
261 
262 int LdapClient::clientNumber() const
263 {
264  return mClientNumber;
265 }
266 
267 int LdapClient::completionWeight() const
268 {
269  return mCompletionWeight;
270 }
271 
272 void LdapClient::setCompletionWeight( int weight )
273 {
274  mCompletionWeight = weight;
275 }
276 
277 void LdapSearch::readConfig( LdapServer &server, TDEConfig *config, int j, bool active )
278 {
279  TQString prefix;
280  if ( active ) prefix = "Selected";
281  TQString host = config->readEntry( prefix + TQString( "Host%1" ).arg( j ), "" ).stripWhiteSpace();
282  if ( !host.isEmpty() )
283  server.setHost( host );
284 
285  int port = config->readNumEntry( prefix + TQString( "Port%1" ).arg( j ), 389 );
286  server.setPort( port );
287 
288  TQString base = config->readEntry( prefix + TQString( "Base%1" ).arg( j ), "" ).stripWhiteSpace();
289  if ( !base.isEmpty() )
290  server.setBaseDN( base );
291 
292  TQString user = config->readEntry( prefix + TQString( "User%1" ).arg( j ) ).stripWhiteSpace();
293  if ( !user.isEmpty() )
294  server.setUser( user );
295 
296  TQString bindDN = config->readEntry( prefix + TQString( "Bind%1" ).arg( j ) ).stripWhiteSpace();
297  if ( !bindDN.isEmpty() )
298  server.setBindDN( bindDN );
299 
300  TQString pwdBindDN = config->readEntry( prefix + TQString( "PwdBind%1" ).arg( j ) );
301  if ( !pwdBindDN.isEmpty() )
302  server.setPwdBindDN( pwdBindDN );
303 
304  server.setTimeLimit( config->readNumEntry( prefix + TQString( "TimeLimit%1" ).arg( j ) ) );
305  server.setSizeLimit( config->readNumEntry( prefix + TQString( "SizeLimit%1" ).arg( j ) ) );
306  server.setVersion( config->readNumEntry( prefix + TQString( "Version%1" ).arg( j ), 3 ) );
307  server.setSecurity( config->readNumEntry( prefix + TQString( "Security%1" ).arg( j ) ) );
308  server.setAuth( config->readNumEntry( prefix + TQString( "Auth%1" ).arg( j ) ) );
309  server.setMech( config->readEntry( prefix + TQString( "Mech%1" ).arg( j ) ) );
310 }
311 
312 void LdapSearch::writeConfig( const LdapServer &server, TDEConfig *config, int j, bool active )
313 {
314  TQString prefix;
315  if ( active ) prefix = "Selected";
316  config->writeEntry( prefix + TQString( "Host%1" ).arg( j ), server.host() );
317  config->writeEntry( prefix + TQString( "Port%1" ).arg( j ), server.port() );
318  config->writeEntry( prefix + TQString( "Base%1" ).arg( j ), server.baseDN() );
319  config->writeEntry( prefix + TQString( "User%1" ).arg( j ), server.user() );
320  config->writeEntry( prefix + TQString( "Bind%1" ).arg( j ), server.bindDN() );
321  config->writeEntry( prefix + TQString( "PwdBind%1" ).arg( j ), server.pwdBindDN() );
322  config->writeEntry( prefix + TQString( "TimeLimit%1" ).arg( j ), server.timeLimit() );
323  config->writeEntry( prefix + TQString( "SizeLimit%1" ).arg( j ), server.sizeLimit() );
324  config->writeEntry( prefix + TQString( "Version%1" ).arg( j ), server.version() );
325  config->writeEntry( prefix + TQString( "Security%1" ).arg( j ), server.security() );
326  config->writeEntry( prefix + TQString( "Auth%1" ).arg( j ), server.auth() );
327  config->writeEntry( prefix + TQString( "Mech%1" ).arg( j ), server.mech() );
328 }
329 
330 TDEConfig* LdapSearch::config()
331 {
332  if ( !s_config )
333  configDeleter.setObject( s_config, new TDEConfig( "kabldaprc", false, false ) ); // Open read-write, no kdeglobals
334 
335  return s_config;
336 }
337 
338 
339 LdapSearch::LdapSearch()
340  : mActiveClients( 0 ), mNoLDAPLookup( false )
341 {
342  if ( !KProtocolInfo::isKnownProtocol( KURL("ldap://localhost") ) ) {
343  mNoLDAPLookup = true;
344  return;
345  }
346 
347  readConfig();
348  connect(KDirWatch::self(), TQ_SIGNAL(dirty (const TQString&)),this,
349  TQ_SLOT(slotFileChanged(const TQString&)));
350 }
351 
352 void LdapSearch::readWeighForClient( LdapClient *client, TDEConfig *config, int clientNumber )
353 {
354  const int completionWeight = config->readNumEntry( TQString( "SelectedCompletionWeight%1" ).arg( clientNumber ), -1 );
355  if ( completionWeight != -1 )
356  client->setCompletionWeight( completionWeight );
357 }
358 
359 void LdapSearch::updateCompletionWeights()
360 {
361  TDEConfig *config = KPIM::LdapSearch::config();
362  config->setGroup( "LDAP" );
363  for ( uint i = 0; i < mClients.size(); i++ ) {
364  readWeighForClient( mClients[i], config, i );
365  }
366 }
367 
368 void LdapSearch::readConfig()
369 {
370  cancelSearch();
371  TQValueList< LdapClient* >::Iterator it;
372  for ( it = mClients.begin(); it != mClients.end(); ++it )
373  delete *it;
374  mClients.clear();
375 
376  // stolen from KAddressBook
377  TDEConfig *config = KPIM::LdapSearch::config();
378  config->setGroup( "LDAP" );
379  int numHosts = config->readUnsignedNumEntry( "NumSelectedHosts");
380  if ( !numHosts ) {
381  mNoLDAPLookup = true;
382  } else {
383  for ( int j = 0; j < numHosts; j++ ) {
384  LdapClient* ldapClient = new LdapClient( j, this );
385  LdapServer server;
386  readConfig( server, config, j, true );
387  if ( !server.host().isEmpty() ) mNoLDAPLookup = false;
388  ldapClient->setServer( server );
389 
390  readWeighForClient( ldapClient, config, j );
391 
392  TQStringList attrs;
393  // note: we need "objectClass" to detect distribution lists
394  attrs << "cn" << "mail" << "givenname" << "sn" << "objectClass";
395  ldapClient->setAttrs( attrs );
396 
397  connect( ldapClient, TQ_SIGNAL( result( const KPIM::LdapObject& ) ),
398  this, TQ_SLOT( slotLDAPResult( const KPIM::LdapObject& ) ) );
399  connect( ldapClient, TQ_SIGNAL( done() ),
400  this, TQ_SLOT( slotLDAPDone() ) );
401  connect( ldapClient, TQ_SIGNAL( error( const TQString& ) ),
402  this, TQ_SLOT( slotLDAPError( const TQString& ) ) );
403 
404  mClients.append( ldapClient );
405  }
406 
407  connect( &mDataTimer, TQ_SIGNAL( timeout() ), TQ_SLOT( slotDataTimer() ) );
408  }
409  mConfigFile = locateLocal( "config", "kabldaprc" );
410  KDirWatch::self()->addFile( mConfigFile );
411 }
412 
413 void LdapSearch::slotFileChanged( const TQString& file )
414 {
415  if ( file == mConfigFile )
416  readConfig();
417 }
418 
419 void LdapSearch::startSearch( const TQString& txt )
420 {
421  if ( mNoLDAPLookup )
422  return;
423 
424  cancelSearch();
425 
426  int pos = txt.find( '\"' );
427  if( pos >= 0 )
428  {
429  ++pos;
430  int pos2 = txt.find( '\"', pos );
431  if( pos2 >= 0 )
432  mSearchText = txt.mid( pos , pos2 - pos );
433  else
434  mSearchText = txt.mid( pos );
435  } else
436  mSearchText = txt;
437 
438  /* The reasoning behind this filter is:
439  * If it's a person, or a distlist, show it, even if it doesn't have an email address.
440  * If it's not a person, or a distlist, only show it if it has an email attribute.
441  * This allows both resource accounts with an email address which are not a person and
442  * person entries without an email address to show up, while still not showing things
443  * like structural entries in the ldap tree. */
444  TQString filter = TQString( "&(|(objectclass=person)(objectclass=groupOfNames)(mail=*))(|(cn=%1*)(mail=%2*)(mail=*@%3*)(givenName=%4*)(sn=%5*))" )
445  .arg( mSearchText ).arg( mSearchText ).arg( mSearchText ).arg( mSearchText ).arg( mSearchText );
446 
447  TQValueList< LdapClient* >::Iterator it;
448  for ( it = mClients.begin(); it != mClients.end(); ++it ) {
449  (*it)->startQuery( filter );
450  kdDebug(5300) << "LdapSearch::startSearch() " << filter << endl;
451  ++mActiveClients;
452  }
453 }
454 
455 void LdapSearch::cancelSearch()
456 {
457  TQValueList< LdapClient* >::Iterator it;
458  for ( it = mClients.begin(); it != mClients.end(); ++it )
459  (*it)->cancelQuery();
460 
461  mActiveClients = 0;
462  mResults.clear();
463 }
464 
465 void LdapSearch::slotLDAPResult( const KPIM::LdapObject& obj )
466 {
467  mResults.append( obj );
468  if ( !mDataTimer.isActive() )
469  mDataTimer.start( 500, true );
470 }
471 
472 void LdapSearch::slotLDAPError( const TQString& )
473 {
474  slotLDAPDone();
475 }
476 
477 void LdapSearch::slotLDAPDone()
478 {
479  if ( --mActiveClients > 0 )
480  return;
481 
482  finish();
483 }
484 
485 void LdapSearch::slotDataTimer()
486 {
487  TQStringList lst;
488  LdapResultList reslist;
489  makeSearchData( lst, reslist );
490  if ( !lst.isEmpty() )
491  emit searchData( lst );
492  if ( !reslist.isEmpty() )
493  emit searchData( reslist );
494 }
495 
496 void LdapSearch::finish()
497 {
498  mDataTimer.stop();
499 
500  slotDataTimer(); // emit final bunch of data
501  emit searchDone();
502 }
503 
504 void LdapSearch::makeSearchData( TQStringList& ret, LdapResultList& resList )
505 {
506  TQString search_text_upper = mSearchText.upper();
507 
508  TQValueList< KPIM::LdapObject >::ConstIterator it1;
509  for ( it1 = mResults.begin(); it1 != mResults.end(); ++it1 ) {
510  TQString name, mail, givenname, sn;
511  TQStringList mails;
512  bool isDistributionList = false;
513  bool wasCN = false;
514  bool wasDC = false;
515 
516  //kdDebug(5300) << "\n\nLdapSearch::makeSearchData()\n\n" << endl;
517 
518  LdapAttrMap::ConstIterator it2;
519  for ( it2 = (*it1).attrs.begin(); it2 != (*it1).attrs.end(); ++it2 ) {
520  TQByteArray val = (*it2).first();
521  int len = val.size();
522  if( len > 0 && '\0' == val[len-1] )
523  --len;
524  const TQString tmp = TQString::fromUtf8( val, len );
525  //kdDebug(5300) << " key: \"" << it2.key() << "\" value: \"" << tmp << "\"" << endl;
526  if ( it2.key() == "cn" ) {
527  name = tmp;
528  if( mail.isEmpty() )
529  mail = tmp;
530  else{
531  if( wasCN )
532  mail.prepend( "." );
533  else
534  mail.prepend( "@" );
535  mail.prepend( tmp );
536  }
537  wasCN = true;
538  } else if ( it2.key() == "dc" ) {
539  if( mail.isEmpty() )
540  mail = tmp;
541  else{
542  if( wasDC )
543  mail.append( "." );
544  else
545  mail.append( "@" );
546  mail.append( tmp );
547  }
548  wasDC = true;
549  } else if( it2.key() == "mail" ) {
550  mail = tmp;
551  LdapAttrValue::ConstIterator it3 = it2.data().begin();
552  for ( ; it3 != it2.data().end(); ++it3 ) {
553  mails.append( TQString::fromUtf8( (*it3).data(), (*it3).size() ) );
554  }
555  } else if( it2.key() == "givenName" )
556  givenname = tmp;
557  else if( it2.key() == "sn" )
558  sn = tmp;
559  else if( it2.key() == "objectClass" &&
560  (tmp == "groupOfNames" || tmp == "kolabGroupOfNames") ) {
561  isDistributionList = true;
562  }
563  }
564 
565  if( mails.isEmpty()) {
566  if ( !mail.isEmpty() ) mails.append( mail );
567  if( isDistributionList ) {
568  //kdDebug(5300) << "\n\nLdapSearch::makeSearchData() found a list: " << name << "\n\n" << endl;
569  ret.append( name );
570  // following lines commented out for bugfixing kolab issue #177:
571  //
572  // Unlike we thought previously we may NOT append the server name here.
573  //
574  // The right server is found by the SMTP server instead: Kolab users
575  // must use the correct SMTP server, by definition.
576  //
577  //mail = (*it1).client->base().simplifyWhiteSpace();
578  //mail.replace( ",dc=", ".", false );
579  //if( mail.startsWith("dc=", false) )
580  // mail.remove(0, 3);
581  //mail.prepend( '@' );
582  //mail.prepend( name );
583  //mail = name;
584  } else {
585  //kdDebug(5300) << "LdapSearch::makeSearchData() found BAD ENTRY: \"" << name << "\"" << endl;
586  continue; // nothing, bad entry
587  }
588  } else if ( name.isEmpty() ) {
589  //kdDebug(5300) << "LdapSearch::makeSearchData() mail: \"" << mail << "\"" << endl;
590  ret.append( mail );
591  } else {
592  //kdDebug(5300) << "LdapSearch::makeSearchData() name: \"" << name << "\" mail: \"" << mail << "\"" << endl;
593  ret.append( TQString( "%1 <%2>" ).arg( name ).arg( mail ) );
594  }
595 
596  LdapResult sr;
597  sr.clientNumber = (*it1).client->clientNumber();
598  sr.completionWeight = (*it1).client->completionWeight();
599  sr.name = name;
600  sr.email = mails;
601  resList.append( sr );
602  }
603 
604  mResults.clear();
605 }
606 
607 bool LdapSearch::isAvailable() const
608 {
609  return !mNoLDAPLookup;
610 }
611 
612 
613 #include "ldapclient.moc"
This class is internal.
Definition: ldapclient.h:143
void result(const KPIM::LdapObject &)
void setAttrs(const TQStringList &attrs)
Definition: ldapclient.cpp:93
TQStringList attrs() const
Definition: ldapclient.h:164
void error(const TQString &)
void startQuery(const TQString &filter)
Definition: ldapclient.cpp:105
This class is internal.
Definition: ldapclient.h:106
void searchData(const TQStringList &)
Results, assembled as "Full Name <email>" (This signal can be emitted many times)
TDEPIM classes for drag and drop of mails.
Structure describing one result returned by a LDAP query.
Definition: ldapclient.h:230
TQString name
full name
Definition: ldapclient.h:231
TQStringList email
emails
Definition: ldapclient.h:232
int completionWeight
for sorting in a completion list
Definition: ldapclient.h:234
int clientNumber
for sorting in a ldap-only lookup
Definition: ldapclient.h:233