update-fts-index.pl [plain text]
use strict;
use Getopt::Long;
use IPC::Open3;
use Sys::Syslog qw(:standard :macros);
use Errno;
sub usage
{
die <<EOT;
Usage: $0 [options] username ...
or: $0 [options] --queued
Options:
--mailbox name update only this mailbox, not all mailboxes;
multiple --mailbox arguments allowed
--quiet
--syslog log to syslog not stdout/stderr
--verbose
EOT
}
my %opts;
GetOptions(\%opts,
'mailbox=s@',
'queued',
'quiet',
'syslog',
'verbose',
) || usage();
if ((@ARGV == 0 && !defined($opts{queued})) ||
(@ARGV > 0 && defined($opts{queued}))) {
usage();
}
if ($opts{syslog}) {
my $ident = $0;
$ident =~ s,.*/,,;
openlog($ident, "pid", "dovecot") or die("openlog: $!\n");
}
if ($> != 0) {
myfatal("must run as root");
}
my $queue_dir = "/private/var/db/dovecot.fts.update";
$ENV{PATH} = "/usr/bin:/bin:/usr/sbin:/sbin:/Applications/Server.app/Contents/ServerRoot/usr/bin:/Applications/Server.app/Contents/ServerRoot/usr/sbin";
delete $ENV{CDPATH};
my $conf = `/Applications/Server.app/Contents/ServerRoot/usr/bin/doveconf -h mail_plugins`;
chomp $conf;
my $noop = 0;
unless (grep { $_ eq "fts" } split(/\s+/, $conf)) {
myinfo("Full-text search capability disabled; not doing anything.")
if $opts{verbose};
$noop = 1;
}
my $ok = 1;
if (defined($opts{queued})) {
sleep(10);
opendir(DIR, $queue_dir) or myfatal("$queue_dir: $!");
my @entries = readdir(DIR);
closedir(DIR);
my %work;
for (@entries) {
next if $_ eq "." or $_ eq "..";
if (!/^(\.?([a-zA-Z0-9%]+)\.([a-zA-Z0-9%]+))$/) {
mywarn("$queue_dir/$_: malformed or unsafe name");
next;
}
my $name = $1;
my $user = $2;
my $mailbox = $3;
next unless defined $user and defined $mailbox;
$user =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge;
$mailbox =~ s/%([a-fA-F0-9]{2})/chr(hex($1))/ge;
push @{$work{$user}->{mailboxes}}, $mailbox;
$work{$user}->{queuefiles}->{$mailbox} = $name;
$work{$user}->{order} = rand;
}
my @order = sort { $work{$a}->{order} <=> $work{$b}->{order} }
keys %work;
for my $user (@order) {
if ($noop ||
update_fts_with_retries($user, \&preserve_queuefile_for,
\&delete_queuefile_for,
$work{$user},
@{$work{$user}->{mailboxes}}) <= 0) {
$ok = 0;
for (keys %{$work{$user}->{queuefiles}}) {
my $queuefile = $work{$user}->{queuefiles}->{$_};
if (!unlink("$queue_dir/$queuefile")) {
mywarn("$queue_dir/$queuefile: $!")
unless $!{ENOENT};
}
}
}
}
}
if ($noop) {
exit 0;
}
if (!defined($opts{queued})) {
for (@ARGV) {
my @mailboxes;
if (defined($opts{mailbox})) {
@mailboxes = @{$opts{mailbox}};
} else {
@mailboxes = ();
}
/(.+)/;
my $user = $1;
if (update_fts($user, undef, undef, undef, @mailboxes) <= 0) {
$ok = 0;
}
}
}
if (!$opts{quiet}) {
my $disp = $ok ? "Done" : "Failed";
myinfo($disp);
}
exit !$ok;
sub update_fts_with_retries
{
my @args = @_;
for (my $tries = 3; --$tries >= 0; ) {
my $r = update_fts(@args);
return $r if $r >= 0;
if ($tries > 0) {
myinfo("Will retry in a minute");
sleep(60);
} else {
myinfo("Giving up");
}
}
return 0;
}
sub update_fts
{
my $user = shift;
my $preupdate_func = shift;
my $postupdate_func = shift;
my $func_context = shift;
my @mailboxes = @_;
if (!$opts{quiet}) {
myinfo("Updating search indexes for user $user");
}
if (@mailboxes == 0) {
my @list;
return 0 unless doveadm(\@list, "mailbox", "list", "-u", $user);
return 0 unless @list;
for (@list) {
push @mailboxes, $1 if /(.+)/;
}
}
my $ok = 1;
for my $boxi (0..$#mailboxes) {
if (!$opts{quiet}) {
myinfo("Updating search index for user $user" .
" mailbox " . ($boxi + 1) .
" of " . scalar(@mailboxes));
}
my $mailbox = $mailboxes[$boxi];
&$preupdate_func($func_context, $mailbox)
if defined $preupdate_func;
$ok = 0 unless doveadm(undef, "index", "-u", $user, $mailbox);
&$postupdate_func($func_context, $mailbox)
if defined $postupdate_func;
}
if (!$opts{quiet}) {
myinfo("Compacting search indexes for user $user");
}
$ok = 0 unless doveadm(undef, "fts", "optimize", "-u", $user);
return $ok;
}
sub doveadm
{
my $outref = shift;
my @args = @_;
my $doveadm = "/Applications/Server.app/Contents/ServerRoot/usr/bin/doveadm";
unshift @args, "-v" if $opts{verbose};
unshift @args, $doveadm;
print "> " . join(" ", @args) . "\n" if $opts{verbose};
my $pid = open3(\*TO_DOVEADM, \*FROM_DOVEADM, \*FROM_DOVEADM, @args);
if (!defined($pid)) {
mywarn("'" . join(" ", @args) . "' failed: $!");
return 0;
}
close(TO_DOVEADM);
my @errs;
while (my $line = <FROM_DOVEADM>) {
chomp $line;
print "< $line\n" if $opts{verbose};
if ($line =~ /(Debug|Info|Warning|Error|Fatal|Panic):/) {
push @errs, $line;
} elsif (defined($outref)) {
push @$outref, $line;
}
}
close(FROM_DOVEADM);
waitpid($pid, 0);
my $status = $?;
if ($status != 0) {
for (@errs) {
chomp;
mywarn("$doveadm: $_");
}
mywarn("'" . join(" ", @args) . "' failed: $status");
return 0;
}
return 1;
}
sub preserve_queuefile_for
{
my $userref = shift;
my $mailbox = shift;
my $queuefile = $userref->{queuefiles}->{$mailbox};
if (defined($queuefile) && $queuefile !~ /^\./ &&
!rename("$queue_dir/$queuefile", "$queue_dir/.$queuefile")) {
mywarn("rename $queue_dir/$queuefile -> $queue_dir/.$queuefile: $!")
unless $!{ENOENT};
}
}
sub delete_queuefile_for
{
my $userref = shift;
my $mailbox = shift;
my $queuefile = $userref->{queuefiles}->{$mailbox};
$queuefile = ".$queuefile" unless $queuefile =~ /^\./;
if (defined($queuefile) && !unlink("$queue_dir/$queuefile")) {
mywarn("$queue_dir/$queuefile: $!") unless $!{ENOENT};
}
}
sub myfatal
{
my $msg = shift;
if ($opts{syslog}) {
syslog(LOG_ERR, $msg);
}
die("$msg\n");
}
sub mywarn
{
my $msg = shift;
if ($opts{syslog}) {
syslog(LOG_WARNING, $msg);
} else {
warn(scalar(localtime) . ": $msg\n");
}
}
sub myinfo
{
my $msg = shift;
if ($opts{syslog}) {
syslog(LOG_INFO, $msg);
} else {
print scalar(localtime) . ": $msg\n";
}
}