1#! @PERL@ -w 2# 3# Copyright (C) 2000-2006 Marc de Courville <marc@courville.org> 4# Copyright (C) 2009 "Alexandra N. Kossovsky" <sasha@oktetlabs.ru> 5# Copyright (C) 2005-2016 Roland Rosenfeld <roland@spinnaker.de> 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software Foundation, 19# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301,, USA. 20 21use strict; 22use Getopt::Long; 23use Net::LDAP; 24use Pod::Usage; 25# install libgssapi-perl libauthen-sasl-perl 26use Authen::SASL qw(Perl); 27 28#------8<------8<------8<------8<---cut here--->8------>8------>8------>8------ 29# The defaults 30my $man = 0; 31my $help = 0; 32my $DEBUG = 0; 33my $lbdb_output = 0; 34my $version = 0; 35my $ldap_server_nickname = ''; 36my $config_file = ''; 37my $prefix = '@prefix@'; 38 39# hostname of your ldap server 40our $ldap_server = 'ldap.four11.com'; 41# ldap base search 42our $search_base = 'c=US'; 43# list of the fields that will be used for the query 44our $ldap_search_fields = 'givenname sn cn'; 45# list of the fields that will be used for composing the answer 46our $ldap_expected_answers = 'givenname sn mail o'; 47# format of the email result based on the expected answers of the ldap query 48our $ldap_result_email = '${mail}'; 49# format of the realname result based on the expected answers of the ldap query 50our $ldap_result_realname = '${givenname} ${sn}'; 51# format of the comment result based on the expected answers of the ldap query 52our $ldap_result_comment = '(${o})'; 53# use ignorant (wildcard searching): 54our $ignorant = 0; 55# LDAP bind DN: 56our $ldap_bind_dn = ''; 57# LDAP bind password: 58our $ldap_bind_password = ''; 59# LDAP TLS 60our $ldap_tls = 0; 61# LDAP SASL mechanism 62our $ldap_sasl_mech = ''; 63 64our %ldap_server_db = ( 65 'four11' => ['ldap.four11.com', 'c=US', 'givenname sn cn mail', 'givenname cn sn mail o', '${mail}', '${givenname} ${sn}', '${o}' ], 66 'infospace' => ['ldap.infospace.com', 'c=US', 'givenname sn cn mail', 'givenname cn sn mail o', '${mail}', '${givenname} ${sn}', '${o}' ], 67 'whowhere' => ['ldap.whowhere.com', 'c=US', 'givenname sn cn mail', 'givenname cn sn mail o', '${mail}', '${givenname} ${sn}', '${o}' ], 68 'bigfoot' => ['ldap.bigfoot.com', 'c=US', 'givenname sn cn mail', 'givenname cn sn mail o', '${mail}', '${givenname} ${sn}', '${o}' ], 69 'switchboard' => ['ldap.switchboard.com', 'c=US', 'givenname sn cn mail', 'givenname cn sn mail o', '${mail}', '${givenname} ${sn}', '${o}' ], 70 'infospacebiz' => ['ldapbiz.infospace.com', 'c=US', 'givenname sn cn mail', 'givenname cn sn mail o', '${mail}', '${givenname} ${sn}', '${o}' ] 71); 72#------8<------8<------8<------8<---cut here--->8------>8------>8------>8------ 73 74sub versionstring { 75 my $ver = '@LBDB_VERSION@'; 76 return($ver . " <marc\@courville.org>"); 77} 78 79# Source a perl file 80sub process_file { 81 foreach my $file (@_) { 82 if (-r $file) { 83 unless (my $return = do $file) { 84 warn "couldn't parse $file: $@" if $@; 85 warn "couldn't do $file: $!" unless defined $return; 86 warn "couldn't run $file" unless $return; 87 } 88 } 89# else { 90# warn "either $file doesn't exist or is not readable by me!\n"; 91# } 92 } 93} 94 95# first we need to apply defaults 96process_file("@SYSCONF_DIR@/lbdb_ldap.rc", 97 "@SYSCONF_DIR@/mutt_ldap_query.rc", 98 "$ENV{HOME}/.lbdb/ldap.rc", 99 "$ENV{HOME}/.mutt_ldap_query.rc"); 100 101 102# Parse command line options. They override system defaults. 103GetOptions ( 104 'config_file|c=s' => \$config_file, 105 'server|ls=s' => \$ldap_server, 106 'search_base|sb:s' => \$search_base, 107 'search_fields|sf:s' => \$ldap_search_fields, 108 'expected_answers|ea:s' => \$ldap_expected_answers, 109 'format_email|fe:s' => \$ldap_result_email, 110 'format_realname|fr:s' => \$ldap_result_realname, 111 'format_comment|fc:s' => \$ldap_result_comment, 112 'nickname|n=s' => \$ldap_server_nickname, 113 'bind_dn|bd:s' => \$ldap_bind_dn, 114 'bind_password|bp:s' => \$ldap_bind_password, 115 'tls:s' => \$ldap_tls, 116 'sasl_mech|sm:s' => \$ldap_sasl_mech, 117 'debug' => sub { $DEBUG = 1 }, 118 'help|?|h' => \$help, 119 'man|m' => \$man, 120 'ignorant|i' => \$ignorant, 121 'lbdb_output|l' => \$lbdb_output, 122 'version|v' => \$version 123); 124 125# 126# print usage and help info before we process config files 127# 128pod2usage(1) if $help; 129pod2usage(-verbose => 2) if $man; 130 131if ($version) { 132 print "mutt_ldap_query version " . &versionstring() . "\n"; 133 exit(0); 134} 135 136# command-line config file take precedence over command-line options 137if ($config_file) { 138 process_file($config_file); 139} 140 141 142# after we've done with GetOptions $ARGV[0] should present the rest 143# (i.e. mandatory search pattern) 144die pod2usage(1) if (! $ARGV[0] ); 145 146if ($ldap_server_nickname) { 147 my $option_array = $ldap_server_db{$ldap_server_nickname}; 148 die print "$0 unknown server nickname:\n\t no server associated to the nickname $ldap_server_nickname, please modify the internal database according your needs by editing the ressource file or specifying the relevant one to use\n" if ! $option_array; 149 $ldap_server = $option_array->[0]; 150 $search_base = $option_array->[1]; 151 $ldap_search_fields = $option_array->[2]; 152 $ldap_expected_answers = $option_array->[3]; 153 $ldap_result_email = $option_array->[4]; 154 $ldap_result_realname = $option_array->[5]; 155 $ldap_result_comment = $option_array->[6]; 156 if (defined($option_array->[7])) { 157 $ignorant = $option_array->[7]; 158 } 159 if (defined($option_array->[8])) { 160 $ldap_bind_dn = $option_array->[8]; 161 } 162 if (defined($option_array->[9])) { 163 $ldap_bind_password = $option_array->[9]; 164 } 165 if (defined($option_array->[10])) { 166 $ldap_tls = $option_array->[10]; 167 } 168 if (defined($option_array->[11])) { 169 $ldap_sasl_mech = $option_array->[11]; 170 } 171} 172 173print "DEBUG: ldap_server='$ldap_server' search_base='$search_base' search_fields='$ldap_search_fields'\ 174 expected_answer='$ldap_expected_answers' format_email='$ldap_result_email' format_realname='$ldap_result_realname'\ 175 bind_dn='$ldap_bind_dn' bind_password='$ldap_bind_password'\n" if ($DEBUG); 176 177my @fields = split / /, $ldap_search_fields; 178my @results; 179 180foreach my $askfor ( @ARGV ) { 181 my $query=""; 182 if ($ignorant) { 183# enable this if you want to include wildcard in your search with some huge 184# ldap databases you might want to avoid it 185 $query = join '', map { "($_=*$askfor*)" } @fields; 186 } 187 else { 188 $query = join '', map { "($_=$askfor)" } @fields; 189 } 190 $query = "(|" . $query . ")"; 191 192 print "DEBUG: perl ldap module processing filter:\nDEBUG: $query\n" if ($DEBUG); 193 my $ldap = Net::LDAP->new($ldap_server, Debug => 3) or die $@; 194 if (defined($ldap_tls) && $ldap_tls) { 195 my $mesg = $ldap->start_tls(); 196 die $mesg->error if $mesg->is_error; 197 } 198 if (defined($ldap_sasl_mech) && $ldap_sasl_mech ne '') { 199 my $sasl = Authen::SASL->new(mechanism => 'GSSAPI') or die $@; 200 my $mesg =$ldap->bind(sasl => $sasl); 201 die $mesg->error if $mesg->is_error; 202 } 203 elsif (defined($ldap_bind_dn) && $ldap_bind_dn ne '' 204 && defined($ldap_bind_password) && $ldap_bind_password ne '') { 205 $ldap->bind($ldap_bind_dn, password=> $ldap_bind_password); 206 } else { 207 $ldap->bind; 208 } 209 my $mesg = $ldap->search( base => $search_base, filter => $query ) or die $@; 210 if ($mesg->code && ($mesg->code ne '4')) { 211 die "Search failed. LDAP server returned an error : ", $mesg->code, ", description: ", $mesg->error; 212 } 213 my @entries = $mesg->entries; 214 map { $_->dump } $mesg->all_entries if ($DEBUG); 215 my $entry; 216 foreach $entry (@entries) { 217 print "DEBUG: processing $entry->dn\n" if ($DEBUG); 218# prepare the results 219 my @emails = (); 220 my $realname = $ldap_result_realname; 221 my $comment = $ldap_result_comment; 222 foreach my $answer (split / /, $ldap_expected_answers) { 223 my $result = ''; 224# if this is email we take all the values 225 if( $ldap_result_email =~ /\$\{$answer}/ ) { 226 foreach my $result ($entry->get_value($answer)) { 227 my $email = $ldap_result_email; 228 $email =~ s/\$\{$answer}/$result/g; 229 push @emails, $email; 230 } 231 } 232 else { 233 my $result = ''; 234# if there is no answer must return the null otherwise we get an uninitialized variable error 235 ($result = $entry->get_value($answer)) || ($result = ''); 236# replace the containers with the results of the query 237 $realname =~ s/\$\{$answer}/$result/g; 238 $comment =~ s/\$\{$answer}/$result/g; 239 } 240 } 241 foreach my $ema (@emails) { 242 push @results, "$ema\t$realname\t$comment\n"; 243 } 244 } 245 $ldap->unbind; 246} 247 248if ($lbdb_output) { 249# display results convenient for lbdb processing 250 print @results; 251} 252else { 253 print "LDAP query: found ", scalar(@results), "\n", @results; 254} 255 256exit 1 if ! @results; 257 258__END__ 259 260=head1 NAME 261 262mutt_ldap_query - Query LDAP server for Mutt mail-reader 263 264=head1 SYNOPSIS 265 266mutt_ldap_query.pl [options] <name_to_query> [[<other_name_to_query>] ...] 267 268=head1 OPTIONS 269 270=over 8 271 272=item B<--config=config_file> or B<-c config_file> 273 274specify an alternate resource file other than the system ones 275(F<@SYSCONF_DIR@/lbdb_ldap.rc> or F<@SYSCONF_DIR@/mutt_ldap_query.rc>) 276or default personal ones (F<$HOME/.lbdb/ldap.rc> or 277F<$HOME/.mutt_ldap_query.rc>). 278 279=item B<--server=ldap_server> or B<-ls ldap_server> 280 281hostname of your ldap server. You can also use an ldap://foo[:port] 282or ldaps://foo[:port] URL here to talk to different ports or SSL 283encrypted servers. Or use ldapi://%2fvar%2flib%2fldap_sock to access an 284ldap server via a socket (use %2f as as replacement for a slash in the file 285name). 286 287=item B<--search_base=ldap_search_base> or B<-sb ldap_search_base> 288 289use <search_base> as the starting point for the search instead of the default. 290 291=item B<--search_fields=ldap_search_fields> or B<-sf ldap_search_fields> 292 293list of the fields on which the query will be performed. 294 295=item B<--expected_answers=ldap_expected_answers> or B<-ea ldap_expected_answers> 296 297list of the fields expected as the answer of the ldap server that will 298be used for composing the output of the script. 299 300=item B<--format_email=result_format_email> or B<-fe result_format_email> 301 302format to be used for composing the email output result. It has to be 303based on the expected ldap server answers and can use variable 304containers of the form ${variable} where variable belongs to the 305<ldap_expected_answers> set. 306 307=item B<--format_realname=result_format_realname> or B<-fr result_format_realname> 308 309format to be used for composing the realname output result. It has to 310be based on the expected ldap server answers and can use variable 311containers of the form ${variable} where variable belongs to the 312<ldap_expected_answers> set. 313 314=item B<--format_comment=result_format_comment> or B<-fc result_format_comment> 315 316format to be used for composing the comment output result. It has to 317be based on the expected ldap server answers and can use variable 318containers of the form ${variable} where variable belongs to the 319<ldap_expected_answers> set. 320 321=item B<--bind_dn=bind_distinguished_name> or B<-bd bind_distinguished_name> 322 323the destinguished name of the user who binds to the LDAP server. 324Leave it empty for an anonmyous bind. 325 326=item B<--bind_password=secret> or B<-bp secret> 327 328the bind password for binding to the LDAP server. 329Leave it empty for an anonmyous bind. 330 331=item B<--tls=1> or B<--tls=0> 332 333enable or disable transport layer security (TLS). 334 335=item B<--sasl_mech=mechanism> or B<-sm mechanism> 336 337the SASL mechanism, for example GSSAPI (empty string to turn off). 338 339=item B<--nickname=ldap_server_nickname> or B<-n ldap_server_nickname> 340 341shortcut for avoiding to use all the previous options by using the 342script builtin or alternate config file table of common servers and 343associated options. All the required parameters are then derived by 344performing a <lbdb_server_nickname> lookup. 345 346=item B<--debug> or B<-d> 347 348turn on debugging messages. 349 350=item B<--help> or B<-?> or B<-h> or B<--man> or B<-m> 351 352generates this help message. 353 354=item B<--ignorant> or B<-i> 355 356ignorant mode: search using wildcard for *name_to_query* (requires a 357longer processing from LDAP server but is quite convenient :). 358 359=item B<--lbdb_output> or B<-l> 360 361suppress number of matches output (suited for interfacing with little 362brother database http://www.spinnaker.de/lbdb/). 363 364=item B<--version> or B<-v> 365 366show the version. 367 368=back 369 370=head1 DESCRIPTION 371 372B<mutt_ldap_query> performs ldap queries using either ldapsearch command 373or the perl-ldap module and it outputs the required formatted data for 374feeding mutt when using its "External Address Query" feature. 375 376The output of the script consists in 3 fields separated with tabs: the 377email address, the name of the person and a comment. 378 379=head1 INTERFACING WITH MUTT 380 381This perl script can be interfaced with mutt by defining in your .muttrc: 382 383 set query_command = "mutt_ldap_query.pl %s" 384 385Multiple requests are supported: the "Q" command of mutt accepts as argument 386a list of queries (e.g. "Gosse de\ Courville"). 387 388Alternatively mutt_ldap_query can be interfaced with the more generic 389little brother database query program (http://www.spinnaker.de/lbdb/) 390using: 391 392 set query_command = "lbdbq %s" 393 394and by specifying in your ~/.lbdb/lbdbrc file another method of query 395just adding to the METHODS variable the m_ldap module e.g.: 396 397 METHODS='m_inmail m_passwd m_ldap m_muttalias m_finger' 398 399and the right path to access m_ldap in MODULES_PATH, e.g. if you moved 400F<m_ldap> in F<~/.lbdb/modules>: 401 402 MODULES_PATH="/usr/local/lib $HOME/.lbdb/modules" 403 404Just make sure to use the correct path for calling mutt_ldap_query 405in the m_ldap script. 406 407=head1 RESOURCE FILE FORMAT 408 409mutt_ldap_query is now fully customizable using an external resource 410file. By default mutt_ldap_query parses the system definition file 411located generally at F</etc/mutt_ldap_query.rc> or 412F</usr/local/etc/mutt_ldap_query.rc> and also the user one: 413F<$HOME/.mutt_ldap_query.rc>. 414 415Instead of using command line options, the user can redefine all the 416variables using the resource file by two manners in order to match his 417site configuration. A file example is provided below: 418 419 # The format of each entry of the ldap server database is the following: 420 # LDAP_NICKNAME => ['LDAP_SERVER', 421 # 'LDAP_SEARCH_BASE', 422 # 'LDAP_SEARCH_FIELDS', 423 # 'LDAP_EXPECTED_ANSWERS', 424 # 'LDAP_RESULT_EMAIL', 425 # 'LDAP_RESULT_REALNAME', 426 # 'LDAP_RESULT_COMMENT'], 427 428 # a practical illustrating example being: 429 # debian => ['db.debian.org', 430 # 'ou=users,dc=debian,dc=org', 431 # 'uid cn sn ircnick', 432 # 'uid cn sn ircnick', 433 # '${uid}@debian.org', 434 # '${cn} ${sn}', 435 # '${ircnick}'], 436 # the output of the query will be then: 437 # ${uid}@debian.org\t${cn} ${sn}\t${ircnick} (i.e.: email name comment) 438 439 # warning this database will erase default script builtin 440 %ldap_server_db = ( 441 'four11' => ['ldap.four11.com', 442 'c=US', 443 'givenname sn cn mail', 444 'givenname cn sn mail o', 445 '${mail}', 446 '${givenname} ${sn}', 447 '${o}' ], 448 'infospace' => ['ldap.infospace.com', 449 'c=US', 450 'givenname sn cn mail', 451 'givenname cn sn mail o', 452 '${mail}', 453 '${givenname} ${sn}', 454 '${o}' ], 455 'whowhere' => ['ldap.whowhere.com', 456 'c=US', 457 'givenname sn cn mail', 458 'givenname cn sn mail o', 459 '${mail}', 460 '${givenname} ${sn}', 461 '${o}' ], 462 'bigfoot' => ['ldap.bigfoot.com', 463 'c=US', 464 'givenname sn cn mail' 465 , 'givenname cn sn mail o' 466 , '${mail}' 467 , '${givenname} ${sn}', 468 '${o}' ], 469 'switchboard' => ['ldap.switchboard.com', 470 'c=US', 471 'givenname sn cn mail' 472 , 'givenname cn sn mail o', 473 '${mail}', 474 '${givenname} ${sn}', 475 '${o}' ], 476 'infospacebiz' => ['ldapbiz.infospace.com', 477 'c=US', 478 'givenname sn cn mail', 479 'givenname cn sn mail o', 480 '${mail}', 481 '${givenname} ${sn}', 482 '${o}' ], 483 ); 484 485 # hostname of your ldap server 486 $ldap_server = 'ldap.four11.com'; 487 # ldap base search 488 $search_base = 'c=US'; 489 # list of the fields that will be used for the query 490 $ldap_search_fields = 'givenname sn cn mail'; 491 # list of the fields that will be used for composing the answer 492 $ldap_expected_answers = 'givenname sn cn mail o'; 493 # format of the email result based on the expected answers of the ldap query 494 $ldap_result_email = '${mail}'; 495 # format of the realname result based on the expected answers of the ldap query 496 $ldap_result_realname = '${givenname} ${sn}'; 497 # format of the comment result based on the expected answers of the ldap query 498 $ldap_result_comment = '(${o})'; 499 500=head1 EXAMPLES OF QUERIES 501 502 mutt_ldap_query.pl --ldap_server='ldap.mot.com' \ 503 --search_base='ou=employees, o=Motorola,c=US' \ 504 --ldap_search_fields='commonName gn sn cn uid' \ 505 --ldap_expected_answers='gn sn preferredRfc822Recipient ou c telephonenumber' \ 506 --ldap_result_email='${preferredRfc822Recipient}' \ 507 --ldap_result_realname='${gn} ${sn}' \ 508 --ldap_result_comment='(${telephonenumber}) ${ou} ${c}' \ 509 Gosse de\ Courville 510 511performs a query using the ldap server ldap.mot.com using 512the following searching base 'ou=employees, o=Motorola,c=US' and 513performing a search on the fields 'commonName gn sn cn uid' for 'Gosse' 514and then "de Courville" looking for the following answers 'gn sn 515preferredRfc822Recipient ou c telephonenumber'. Based on this answers, 516mutt_ldap_query will return a list of entries identified of the form: 517 518 <${preferredRfc822Recipient}>\t${gn} ${sn}\t(${telephonenumber}) ${ou} ${c} 519 520where ${} variables should be considered as containers that are 521replaced by the results of the query. The previous query can be 522greatly simplified by using the ldap server mini database feature of 523the resource file introducing for example a nickname. 524 525 mutt_ldap_query.pl --ldap_server_nickname='motorola' Gosse de\ Courville 526 527When not sure of the full name (i.e. it should contain Courville) 528the ignorant mode is useful since the query will be performed using 529wildcards, i.e. *Courville* in the following case: 530 531 mutt_ldap_query.pl --ignorant Courville 532 533=head1 WHERE TO GET IT 534 535The latest version can be retrieved at 536 ftp://ftp.mutt.org/pub/mutt/contrib 537or 538 http://www.courville.org/ 539 540Note that now the script is integrated in the latest version of the little brother database available at http://www.spinnaker.de/lbdb/. It is thus easier to use through this standard package than to hand customize it to fit your system/distribution needs. 541 542=head1 REFERENCES 543 544=over 2 545 546=item - 547 548perl-ldap module 549 http://perl-ldap.sourceforge.net/ 550 551=item - 552 553mutt is the ultimate email client 554 http://www.mutt.org/ 555 556=item - 557 558historical Brandon Blong's "External Address Query" feature patch for mutt 559 http://www.fiction.net/blong/programs/mutt/#query 560 561=item - 562 563little brother database is an interface query program for mutt that allow 564multiple searches for email addresses based on external query scripts 565just like this one 8-) 566 http://www.spinnaker.de/lbdb/ 567 568=back 569 570=head1 AUTHORS 571 572Marc de Courville <marc@courville.org> and the various other contributors... that kindly sent their patches. 573 574Please report any bugs, or post any suggestions, to <marc@courville.org>. 575 576=head1 COPYRIGHT 577 578Copyright (c) 1998-2003 Marc de Courville <marc@courville.org>. All rights reserved. This program is free software; you can redistribute it and/or modify it under the GNU General Public License (GPL). See http://www.opensource.org/gpl-license.html and http://www.opensource.org/. 579 580=cut 581