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