]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/LDAPCache.pm
LDAPCache: combine ldaps/starttls branches for connect
[pmg-api.git] / src / PMG / LDAPCache.pm
CommitLineData
cad3d400
DM
1package PMG::LDAPCache;
2
3use strict;
4use warnings;
cad3d400
DM
5use File::Path;
6use LockFile::Simple;
c2670481 7use Data::Dumper;
cad3d400
DM
8use DB_File;
9
10use PVE::SafeSyslog;
23b1d0f8 11use PVE::Tools qw(split_list);
cf236104 12use PVE::LDAP;
cad3d400
DM
13
14use PMG::Utils;
23b1d0f8 15use PMG::LDAPConfig;
cad3d400
DM
16
17$DB_HASH->{'cachesize'} = 10000;
18$DB_RECNO->{'cachesize'} = 10000;
19$DB_BTREE->{'cachesize'} = 10000;
20$DB_BTREE->{'flags'} = R_DUP ;
21
3278b571 22my $cachedir = '/var/lib/pmg';
cad3d400
DM
23
24my $last_atime = {};
25my $ldapcache = {};
26
27# DB Description
28#
29# users (hash): UID -> pmail, account, DN
30# dnames (hash): DN -> UID
31# accounts (hash): account -> UID
32# mail (hash): mail -> UID
33# groups (hash): group -> GID
34# memberof (btree): UID -> GID
35#
36my @dbs = ('users', 'dnames', 'groups', 'mails', 'accounts', 'memberof');
37
38sub new {
909eec64 39 my ($self, %args) = @_;
cad3d400
DM
40
41 my $type = ref($self) || $self;
42
909eec64 43 die "undefined ldap id" if !$args{id};
cad3d400
DM
44
45 my $id = $args{id};
cad3d400
DM
46
47 if ($ldapcache->{$id}) {
48 $self = $ldapcache->{$id};
49 } else {
50 $ldapcache->{$id} = $self = bless {}, $type;
51 $self->{id} = $id;
52 }
53
23b1d0f8 54 my $config_properties = PMG::LDAPConfig::properties();
cad3d400 55
23b1d0f8
DC
56 # set defaults for the fields that have one
57 foreach my $property (keys %$config_properties) {
58 my $d = $config_properties->{$property};
59 next if !defined($d->{default});
60 $self->{$property} = $args{$property} || $d->{default};
cad3d400
DM
61 }
62
23b1d0f8
DC
63 # split list returns an array not a reference
64 $self->{accountattr} = [split_list($self->{accountattr})];
65 $self->{mailattr} = [split_list($self->{mailattr})];
b14970ad 66 $self->{groupclass} = [split_list($self->{groupclass})];
23b1d0f8 67
cad3d400
DM
68 $self->{server1} = $args{server1};
69 $self->{server2} = $args{server2};
70 $self->{binddn} = $args{binddn};
71 $self->{bindpw} = $args{bindpw};
72 $self->{basedn} = $args{basedn};
73 $self->{port} = $args{port};
74 $self->{groupbasedn} = $args{groupbasedn};
75 $self->{filter} = $args{filter};
6ad43a10
DC
76 $self->{verify} = $args{verify};
77 $self->{cafile} = $args{cafile};
cad3d400
DM
78
79 if ($args{syncmode} == 1) {
80 # read local data only
81 $self->{errors} = '';
82 $self->loadcache();
83 return $self;
84 }
85
86 return $self if !($args{server1});
87
88 if ($args{syncmode} == 2) {
89 # force sync
90 $self->loaddata(1);
91 } else {
92 $self->loaddata();
93 }
94
95 return $self;
96}
97
98sub lockdir {
99 my ($id) = @_;
100
101 my $dir = "$cachedir/ldapdb_$id";
102 my $scheme = LockFile::Simple->make(
103 -warn => 0, -stale => 1, -autoclean => 1);
104 my $lock = $scheme->lock($dir);
105
106 return $lock;
107}
108
109sub delete {
110 my ($class, $id) = @_;
111
cad3d400
DM
112 if (my $lock = lockdir($id)) {
113 delete $ldapcache->{$id};
114 delete $last_atime->{$id};
115 my $dir = "$cachedir/ldapdb_$id";
116 rmtree $dir;
117 $lock->release;
118 } else {
119 syslog('err' , "can't lock ldap database '$id'");
120 }
121}
122
123sub update {
124 my ($self, $syncmode) = @_;
125
126 if ($syncmode == 1) {
127 # read local data only
128 $self->{errors} = '';
129 $self->loadcache();
130 } elsif ($syncmode == 2) {
131 # force sync
132 $self->loaddata(1);
133 } else {
134 $self->loaddata();
135 }
136}
137
138sub queryusers {
139 my ($self, $ldap) = @_;
140
cad3d400 141
cf236104 142 my $attrs = [ @{$self->{mailattr}}, @{$self->{accountattr}}, 'memberOf' ];
cad3d400 143
cad3d400 144
cf236104
DC
145 my $users = eval { PVE::LDAP::query_users($ldap, $self->{filter}, $attrs, $self->{basedn}) };
146 if (my $err = $@) {
147 $self->{errors} .= "$err\n";
148 syslog('err', $err);
149 return;
150 }
cad3d400 151
cf236104
DC
152 foreach my $user (@$users) {
153 my $dn = $user->{dn};
cad3d400 154
cf236104
DC
155 my $umails = {};
156 my $pmail;
cad3d400 157
cf236104
DC
158 foreach my $attr (@{$self->{mailattr}}) {
159 next if !$user->{attributes}->{$attr};
160 foreach my $mail (@{$user->{attributes}->{$attr}}) {
161 $mail = lc($mail);
162 # Test if the Line starts with one of the following lines:
163 # proxyAddresses: [smtp|SMTP]:
164 # and also discard this starting string, so that $mail is only the
165 # address without any other characters...
cad3d400 166
cf236104 167 $mail =~ s/^(smtp|SMTP)[\:\$]//gs;
cad3d400 168
cf236104
DC
169 if ($mail !~ m/[\{\}\\\/]/ && $mail =~ m/^\S+\@\S+$/) {
170 $umails->{$mail} = 1;
171 $pmail = $mail if !$pmail;
cad3d400
DM
172 }
173 }
cf236104
DC
174 }
175 my $addresses = [ keys %$umails ];
cad3d400 176
cf236104 177 next if !$pmail; # account has no email addresses
cad3d400 178
cf236104
DC
179 my $cuid;
180 $self->{dbstat}->{dnames}->{dbh}->get($dn, $cuid);
181 if (!$cuid) {
182 $cuid = ++$self->{dbstat}->{dnames}->{idcount};
183 $self->{dbstat}->{dnames}->{dbh}->put($dn, $cuid);
184 }
cad3d400 185
cf236104
DC
186 foreach my $attr (@{$self->{accountattr}}) {
187 next if !$user->{attributes}->{$attr};
188 foreach my $account (@{$user->{attributes}->{$attr}}) {
189 next if !defined($account) || !length($account);
cad3d400 190
cf236104
DC
191 $account = lc($account);
192 $self->{dbstat}->{accounts}->{dbh}->put($account, $cuid);
193 my $data = pack('n/a* n/a* n/a*', $pmail, $account, $dn);
194 $self->{dbstat}->{users}->{dbh}->put($cuid, $data);
cad3d400 195 }
cf236104
DC
196 }
197
198 foreach my $mail (@$addresses) {
199 $self->{dbstat}->{mails}->{dbh}->put($mail, $cuid);
200 }
cad3d400 201
cf236104
DC
202 if (!$self->{groupbasedn}) {
203 foreach my $group (@{$user->{groups}}) {
204 my $cgid;
205 $self->{dbstat}->{groups}->{dbh}->get($group, $cgid);
206 if (!$cgid) {
207 $cgid = ++$self->{dbstat}->{groups}->{idcount};
208 $self->{dbstat}->{groups}->{dbh}->put($group, $cgid);
cad3d400 209 }
cf236104 210 $self->{dbstat}->{memberof}->{dbh}->put($cuid, $cgid);
cad3d400
DM
211 }
212 }
cad3d400
DM
213 }
214}
215
216sub querygroups {
217 my ($self, $ldap) = @_;
218
219 return undef if !$self->{groupbasedn};
220
cf236104
DC
221 my $groups = eval { PVE::LDAP::query_groups($ldap, $self->{groupbasedn}, $self->{groupclass}) };
222 if (my $err = $@) {
cad3d400
DM
223 $self->{errors} .= "$err\n";
224 syslog('err', $err);
cf236104 225 return;
cad3d400 226 }
cad3d400 227
cf236104
DC
228 foreach my $group (@$groups) {
229 my $dn = $group->{dn};
cad3d400 230
cf236104
DC
231 my $cgid;
232 $self->{dbstat}->{groups}->{dbh}->get($dn, $cgid);
233 if (!$cgid) {
234 $cgid = ++$self->{dbstat}->{groups}->{idcount};
235 $self->{dbstat}->{groups}->{dbh}->put($dn, $cgid);
236 }
e0dcec3e 237
cf236104
DC
238 foreach my $m (@{$group->{members}}) {
239 my $cuid;
240 $self->{dbstat}->{dnames}->{dbh}->get($m, $cuid);
241 if (!$cuid) {
242 $cuid = ++$self->{dbstat}->{dnames}->{idcount};
243 $self->{dbstat}->{dnames}->{dbh}->put($m, $cuid);
244 }
e0dcec3e 245
cf236104 246 $self->{dbstat}->{memberof}->{dbh}->put($cuid, $cgid);
6ad43a10 247 }
6ad43a10 248 }
cad3d400
DM
249}
250
38527957
DC
251sub ldap_connect {
252 my ($self) = @_;
253
254 my $hosts = [ $self->{server1} ];
255 push @$hosts, $self->{server2} if $self->{server2};
256
257 my $opts = {};
258 my $scheme = $self->{mode};
259
a6f3a53c
DC
260 if ($scheme eq 'ldaps' || $scheme eq 'ldap+starttls') {
261 if ($self->{verify}) {
262 $opts->{verify} = 'require';
263 } elsif ($scheme eq 'ldap+starttls') {
264 $opts->{verify} = 'none';
38527957 265 }
38527957
DC
266
267 if ($self->{cafile}) {
268 $opts->{cafile} = $self->{cafile};
269 } else {
270 $opts->{capath} = '/etc/ssl/certs/';
271 }
272 }
273
274 return PVE::LDAP::ldap_connect($hosts, $scheme, $self->{port}, $opts);
275}
276
b183e761
DM
277sub ldap_connect_and_bind {
278 my ($self) = @_;
279
38527957 280 my $ldap = eval { $self->ldap_connect() };
cf236104 281 die "Can't bind to ldap server '$self->{id}': " . ($@) . "\n" if $@;
b183e761 282
cf236104
DC
283 my $dn;
284 my $pw;
285 $dn = $self->{binddn} if $self->{binddn};
286 $pw = $self->{bindpw} if $self->{bindpw};
287 PVE::LDAP::ldap_bind($ldap, $dn, $pw);
b183e761
DM
288
289 if (!$self->{basedn}) {
290 my $root = $ldap->root_dse(attrs => [ 'defaultNamingContext' ]);
291 $self->{basedn} = $root->get_value('defaultNamingContext');
292 }
293
294 return $ldap;
295}
296
cad3d400
DM
297sub sync_database {
298 my ($self) = @_;
299
300 my $dir = "ldapdb_" . $self->{id};
301 mkdir "$cachedir/$dir";
302
303 # open ldap connection
304
b183e761 305 my $ldap;
cad3d400 306
b183e761
DM
307 eval { $ldap = $self->ldap_connect_and_bind(); };
308 if (my $err = $@) {
cad3d400
DM
309 $self->{errors} .= "$err\n";
310 syslog('err', $err);
311 return;
312 }
313
cad3d400
DM
314 # open temporary database files
315
316 my $olddbh = {};
317
318 foreach my $db (@dbs) {
319 $self->{dbstat}->{$db}->{tmpfilename} = "$cachedir/$dir/${db}_tmp$$.db";
320 $olddbh->{$db} = $self->{dbstat}->{$db}->{dbh};
321 }
322
69278505
DM
323 my $error_cleanup = sub {
324 # close and delete all files
325 foreach my $db (@dbs) {
326 undef $self->{dbstat}->{$db}->{dbh};
327 unlink $self->{dbstat}->{$db}->{tmpfilename};
328 $self->{dbstat}->{$db}->{dbh} = $olddbh->{$db};
329 }
330 };
331
cad3d400
DM
332 eval {
333 foreach my $db (@dbs) {
334 my $filename = $self->{dbstat}->{$db}->{tmpfilename};
335 $self->{dbstat}->{$db}->{idcount} = 0;
336 unlink $filename;
337
338 if ($db eq 'memberof') {
339 $self->{dbstat}->{$db}->{dbh} =
340 tie (my %h, 'DB_File', $filename,
341 O_CREAT|O_RDWR, 0666, $DB_BTREE);
342 } else {
343 $self->{dbstat}->{$db}->{dbh} =
344 tie (my %h, 'DB_File', $filename,
345 O_CREAT|O_RDWR, 0666, $DB_HASH);
346 }
347
348 die "unable to open database file '$filename': $!\n"
349 if !$self->{dbstat}->{$db}->{dbh};
350 }
351 };
69278505
DM
352 if (my $err = $@) {
353 $error_cleanup->();
cad3d400
DM
354 $self->{errors} .= $err;
355 syslog('err', $err);
cad3d400
DM
356 return;
357 }
358
359 $self->querygroups ($ldap) if $self->{groupbasedn};
360
69278505 361 $self->queryusers($ldap) if !$self->{errors};
cad3d400
DM
362
363 $ldap->unbind;
364
365 if ($self->{errors}) {
69278505
DM
366 $error_cleanup->();
367 return;
368 }
cad3d400 369
69278505 370 my $lock = lockdir($self->{id});
cad3d400 371
69278505
DM
372 if (!$lock) {
373 my $err = "unable to get database lock for ldap database '$self->{id}'";
374 $self->{errors} .= "$err\n";
375 syslog('err', $err);
376 $error_cleanup->();
377 return;
378 }
cad3d400 379
69278505
DM
380 foreach my $db (@dbs) {
381 my $filename = $self->{dbstat}->{$db}->{filename} =
382 "$cachedir/$dir/${db}.db";
383 $self->{dbstat}->{$db}->{dbh}->sync(); # flush everything
384 rename $self->{dbstat}->{$db}->{tmpfilename}, $filename;
385 }
cad3d400 386
69278505 387 $lock->release;
cad3d400 388
69278505 389 $last_atime->{$self->{id}} = time();
cad3d400 390
69278505
DM
391 $self->{gcount} = $self->{dbstat}->{groups}->{idcount};
392 $self->{ucount} = __count_entries($self->{dbstat}->{accounts}->{dbh});
393 $self->{mcount} = __count_entries($self->{dbstat}->{mails}->{dbh});
cad3d400
DM
394}
395
396sub __count_entries {
397 my ($dbh) = @_;
398
399 return 0 if !$dbh;
400
401 my $key = 0 ;
402 my $value = "" ;
403 my $count = 0;
404 my $status = $dbh->seq($key, $value, R_FIRST());
405
406 while ($status == 0) {
407 $count++;
408 $status = $dbh->seq($key, $value, R_NEXT());
409 }
410
411 return $count;
412}
413
414sub loadcache {
415 my ($self, $try) = @_;
416
417 my $dir = "ldapdb_" . $self->{id};
418 mkdir "$cachedir/$dir";
419
420 my $filename = "$cachedir/$dir/mails.db";
421
422 return if $last_atime->{$self->{id}} &&
423 PMG::Utils::file_older_than ($filename, $last_atime->{$self->{id}});
424
425 eval {
426 foreach my $db (@dbs) {
427 my $filename = $self->{dbstat}->{$db}->{filename} =
428 "$cachedir/$dir/${db}.db";
429 $self->{dbstat}->{$db}->{idcount} = 0;
430 if ($db eq 'memberof') {
431 $self->{dbstat}->{$db}->{dbh} =
432 tie (my %h, 'DB_File', $filename,
433 O_RDONLY, 0666, $DB_BTREE);
434 } else {
435 $self->{dbstat}->{$db}->{dbh} =
436 tie (my %h, 'DB_File', $filename,
437 O_RDONLY, 0666, $DB_HASH);
438 }
439
440 if (!$self->{dbstat}->{$db}->{dbh} && !$try) {
441 my $err = "ldap error - unable to open database file '$filename': $!";
442 $self->{errors} .= "$err\n";
443 syslog('err', $err) if !$self->{dbstat}->{$db}->{dbh};
444 }
445 }
446 };
447
448 $last_atime->{$self->{id}} = time();
449
450 $self->{gcount} = __count_entries($self->{dbstat}->{groups}->{dbh});
451 $self->{ucount} = __count_entries($self->{dbstat}->{accounts}->{dbh});
452 $self->{mcount} = __count_entries($self->{dbstat}->{mails}->{dbh});
453}
454
455sub loaddata {
456 my ($self, $force) = @_;
457
458 $self->{errors} = '';
459
460 if (!$force) {
461 # only sync if file is older than 1 hour
462
463 my $dir = "ldapdb_" . $self->{id};
464 mkdir "$cachedir/$dir";
465 my $filename = "$cachedir/$dir/mails.db";
466
467 if (-e $filename &&
468 !PMG::Utils::file_older_than($filename, time() - 3600)) {
469 $self->loadcache();
470 return;
471 }
472 }
473
474 $self->sync_database();
475
476 if ($self->{errors}) {
477 $self->loadcache(1);
478 }
479}
480
1d8a50bb 481sub get_groups {
cad3d400
DM
482 my ($self) = @_;
483
1d8a50bb 484 my $res = {};
c2670481 485
cad3d400 486 my $dbh = $self->{dbstat}->{groups}->{dbh};
c2670481
DM
487
488 return $res if !$dbh;
cad3d400
DM
489
490 my $key = 0 ;
491 my $value = "" ;
492 my $status = $dbh->seq($key, $value, R_FIRST());
cad3d400
DM
493
494 while ($status == 0) {
1d8a50bb 495 $res->{$value} = $key;
cad3d400
DM
496 $status = $dbh->seq($key, $value, R_NEXT());
497 }
498
c2670481
DM
499 return $res;
500}
501
1d8a50bb 502sub get_users {
c2670481
DM
503 my ($self) = @_;
504
1d8a50bb 505 my $res = {};
c2670481
DM
506
507 my $dbh = $self->{dbstat}->{users}->{dbh};
508
509 return $res if !$dbh;
510
511 my $key = 0 ;
512 my $value = "" ;
513 my $status = $dbh->seq($key, $value, R_FIRST());
514 my $keys;
515
516 while ($status == 0) {
517 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $value);
1d8a50bb 518 $res->{$key} = {
c2670481
DM
519 pmail => $pmail,
520 account => $account,
521 dn => $dn,
522 };
523 $status = $dbh->seq($key, $value, R_NEXT());
524 }
525
526 return $res;
527}
528
1d8a50bb
DC
529sub get_gid_uid_map {
530 my ($self) = @_;
531
532 my $dbh = $self->{dbstat}->{memberof}->{dbh};
533
534 return [] if !$dbh;
535
536 my $key = 0 ;
537 my $value = "" ;
538
539 my $map = {};
540
541 if($dbh->seq($key, $value, R_FIRST()) == 0) {
542 do {
543 push @{$map->{$value}}, $key;
544 } while($dbh->seq($key, $value, R_NEXT()) == 0);
545 }
546
547 return $map;
548}
549
550sub list_groups {
551 my ($self) = @_;
552
553 my $res = [];
554
555 my $groups = $self->get_groups();
556
557 for my $gid (sort keys %$groups) {
558 push @$res, {
559 dn => $groups->{$gid},
560 gid => $gid,
561 };
562 }
563
564 return $res;
565}
566
567sub list_users {
568 my ($self, $gid) = @_;
569
570 my $res = [];
571
572 my $users = $self->get_users();
573
574 if (!defined($gid)) {
575 $res = [values %$users];
576 } else {
577 my $gid_uid_map = $self->get_gid_uid_map();
578 my $groups = $self->get_groups();
579 die "No such Group ID\n"
580 if !defined($groups->{$gid});
581 my $memberuids = $gid_uid_map->{$gid};
582 for my $uid (@$memberuids) {
583 next if !defined($users->{$uid});
584 push @$res, $users->{$uid};
585 }
586 }
587
588 return $res;
589}
590
c2670481
DM
591sub list_addresses {
592 my ($self, $mail) = @_;
593
594 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
595 my $dbhusers = $self->{dbstat}->{users}->{dbh};
596
597 return undef if !$dbhmails || !$dbhusers;
598
599 $mail = lc($mail);
600
601 my $res = [];
602
603 my $cuid;
604 $dbhmails->get($mail, $cuid);
605 return undef if !$cuid;
606
607 my $rdata;
608 $dbhusers->get($cuid, $rdata);
609 return undef if !$rdata;
610
611 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
612
613 push @$res, { primary => 1, email => $pmail };
614
615 my $key = 0 ;
616 my $value = "" ;
617 my $status = $dbhmails->seq($key, $value, R_FIRST());
618
619 while ($status == 0) {
620 if ($value == $cuid && $key ne $pmail) {
621 push @$res, { primary => 0, email => $key };
622 }
623 $status = $dbhmails->seq($key, $value, R_NEXT());
624 }
625
626 return $res;
cad3d400
DM
627}
628
629sub mail_exists {
630 my ($self, $mail) = @_;
631
632 my $dbh = $self->{dbstat}->{mails}->{dbh};
633 return 0 if !$dbh;
634
635 $mail = lc($mail);
636
637 my $res;
638 $dbh->get($mail, $res);
639 return $res;
640}
641
642sub account_exists {
643 my ($self, $account) = @_;
644
645 my $dbh = $self->{dbstat}->{accounts}->{dbh};
646 return 0 if !$dbh;
647
648 $account = lc($account);
649
650 my $res;
651 $dbh->get($account, $res);
652 return $res;
653}
654
d974ca19
DM
655sub group_exists {
656 my ($self, $group) = @_;
657
658 my $dbh = $self->{dbstat}->{groups}->{dbh};
659 return 0 if !$dbh;
660
661 my $res;
662 $dbh->get($group, $res);
663 return $res;
664}
665
cad3d400
DM
666sub account_has_address {
667 my ($self, $account, $mail) = @_;
668
669 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
670 my $dbhaccounts = $self->{dbstat}->{accounts}->{dbh};
671 return 0 if !$dbhmails || !$dbhaccounts;
672
673 $account = lc($account);
674 $mail = lc($mail);
675
676 my $accid;
677 $dbhaccounts->get($account, $accid);
678 return 0 if !$accid;
679
680 my $mailid;
681 $dbhmails->get($mail, $mailid);
682 return 0 if !$mailid;
683
684 return ($accid == $mailid);
685}
686
687sub user_in_group {
688 my ($self, $mail, $group) = @_;
689
690 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
691 my $dbhgroups = $self->{dbstat}->{groups}->{dbh};
692 my $dbhmemberof = $self->{dbstat}->{memberof}->{dbh};
693
694 return 0 if !$dbhmails || !$dbhgroups || !$dbhmemberof;
695
696 $mail = lc($mail);
697
698 my $cuid;
699 $dbhmails->get($mail, $cuid);
700 return 0 if !$cuid;
701
702 my $groupid;
703 $dbhgroups->get($group, $groupid);
704 return 0 if !$groupid;
705
706 my @gida = $dbhmemberof->get_dup($cuid);
707
708 return grep { $_ eq $groupid } @gida;
709}
710
711sub account_info {
712 my ($self, $mail, $scan) = @_;
713
714 my $dbhmails = $self->{dbstat}->{mails}->{dbh};
715 my $dbhusers = $self->{dbstat}->{users}->{dbh};
716
717 return undef if !$dbhmails || !$dbhusers;
718
719 $mail = lc($mail);
720
721 my $res = {};
722
723 my $cuid;
724 $dbhmails->get($mail, $cuid);
725 return undef if !$cuid;
726
727 my $rdata;
728 $dbhusers->get($cuid, $rdata);
729 return undef if !$rdata;
730
731 my ($pmail, $account, $dn) = unpack('n/a* n/a* n/a*', $rdata);
732
733 $res->{dn} = $dn;
734 $res->{account} = $account;
735 $res->{pmail} = $pmail;
736
737 if ($scan) {
738 my $key = 0 ;
739 my $value = "" ;
740 my $status = $dbhmails->seq($key, $value, R_FIRST());
741 my $mails;
742
743 while ($status == 0) {
744 push @$mails, $key if $value == $cuid;
745 $status = $dbhmails->seq($key, $value, R_NEXT());
746 }
747 $res->{mails} = $mails;
748 }
749
750 return $res;
751}
752
7531;