<NME> Qpsmtpd.pm
<BEF> package Qpsmtpd;
use strict;
#use warnings;

our $VERSION = '0.96';
use vars qw($TraceLevel $Spool_dir $Size_threshold);

use lib 'lib';
use parent 'Qpsmtpd::Base';
use Qpsmtpd::Address;
use Qpsmtpd::Config;
use Qpsmtpd::Constants;

our $hooks = {};
our $LOGGING_LOADED = 0;
my $git = git_version();

sub _restart {
    my $self = shift;
    my %args = @_;
    if ($args{restart}) {

        # reset all global vars to defaults
        $self->conf->clear_cache();
        $hooks          = {};
        $LOGGING_LOADED = 0;
        $TraceLevel     = LOGWARN;
        $Spool_dir      = undef;
        $Size_threshold = undef;
    }
}

sub version { $VERSION . ($git ? "/$git" : '') }

sub git_version {
    return if !-e '.git';
    {
        local $ENV{PATH} = "/usr/bin:/usr/local/bin:/opt/local/bin/";
        $git = `git describe --tags`;
        $git && chomp $git;
    }
    return $git;
}

sub TRACE_LEVEL { $TraceLevel };    # leave for plugin compatibility

sub hooks {
    my ($self, $hook) = @_;
    if ($hook) {
        if (!defined $hooks->{$hook}) { return wantarray ? () : []; };
        return wantarray ? @{$hooks->{$hook}} : $hooks->{$hook};
    };
    return $hooks;
}

sub load_logging {
    my $self = shift;

    return if $LOGGING_LOADED;     # already done
    return if $hooks->{'logging'}; # avoid triggering log activity

    my @plugin_dirs = $self->conf->from_file('plugin_dirs');
    if (!@plugin_dirs) {
        my ($name) = ($0 =~ m!(.*?)/([^/]+)$!);
        @plugin_dirs = ("$name/plugins");
    }

    my @loggers = $self->conf->from_file('logging');
    for my $logger (@loggers) {
        $self->_load_plugin($logger, @plugin_dirs);
        $self->log(LOGINFO, "Loaded $logger");
    }

    $TraceLevel = $self->conf->from_file('loglevel');

    unless (defined($TraceLevel) && $TraceLevel =~ /^\d+$/) {
        $TraceLevel = LOGWARN;    # Default if no loglevel file found.
    }

    $LOGGING_LOADED = 1;

    return @loggers;
}

sub trace_level { return $TraceLevel; }

sub init_logger {                 # needed for compatibility
    shift->trace_level();
}

sub log {
    my ($self, $trace, @log) = @_;
    $self->varlog($trace, join(" ", @log));
}

sub varlog {
    my ($self, $trace) = (shift, shift);
    my ($hook, $plugin, @log);
    if    ($#_ == 0) { (@log) = @_; }                 # log itself
    elsif ($#_ == 1) { ($hook, @log) = @_; }          # plus the hook
    else             { ($hook, $plugin, @log) = @_; } # from a plugin

    $self->load_logging;

    my ($rc) =
      $self->run_hooks_no_respond("logging", $trace, $hook, $plugin, @log)
      or return;

    return if $rc == DECLINED || $rc == OK;              # plugin success
    return if $trace > $TraceLevel;

    # no logging plugins registered, fall back to STDERR
    my $prefix =
        defined $plugin && defined $hook ? " ($hook) $plugin:"
      : defined $plugin ? " $plugin:"
      : defined $hook   ? " ($hook) running plugin:"
      :                   '';

    warn join(' ', $$ . $prefix, @log), "\n";
}

sub conf {
    my $self = shift;
    if (!$self->{_config}) {
        $self->{_config} = Qpsmtpd::Config->new();
    }
    return $self->{_config};
}

sub config {
    my $self = shift;
    return $self->conf->config($self, @_);
}

sub config_dir {
    my $self = shift;
    return $self->conf->config_dir(@_);
}

sub plugin_dirs {
    my $self        = shift;
    my @plugin_dirs = $self->config('plugin_dirs');

    unless (@plugin_dirs) {
        my ($path) = ($ENV{PROCESS} ? $ENV{PROCESS} : $0) =~ m!(.*?)/([^/]+)$!;
        @plugin_dirs = ("$path/plugins");
    }
    return @plugin_dirs;
}

sub load_plugins {
    my $self = shift;

    my @plugins = $self->config('plugins');
    my @loaded;

    if ($hooks->{queue}) {

        #$self->log(LOGWARN, "Plugins already loaded");
        return @plugins;
    }

    for my $plugin_line (@plugins) {
        my $this_plugin = $self->_load_plugin($plugin_line, $self->plugin_dirs);
        push @loaded, $this_plugin if $this_plugin;
    }

    return @loaded;
}

sub _load_plugin {
    my ($self, $plugin_line, @plugin_dirs) = @_;

    # untaint the config data before passing it to plugins
    my ($safe_line) = $plugin_line =~ /^([ -~]+)$/    # all ascii printable
      or die "unsafe characters in config line: $plugin_line\n";
    my ($plugin, @args) = split /\s+/, $safe_line;

    if ($plugin =~ m/::/) {
        return $self->_load_package_plugin($plugin, $safe_line, \@args);
    }

    # regular plugins/$plugin plugin
    my $plugin_name = $plugin;
    $plugin =~ s/:\d+$//;    # after this point, only used for filename

    # Escape everything into valid perl identifiers
    $plugin_name =~ s/([^A-Za-z0-9_\/])/sprintf("_%2x",unpack("C",$1))/eg;

    # second pass cares for slashes and words starting with a digit
    $plugin_name =~ s{
    (/+)       # directory
    (\d?)      # package's first character
    }[
        "::" . (length $2 ? sprintf("_%2x",unpack("C",$2)) : '')
    ]egx;

    my $package = "Qpsmtpd::Plugin::$plugin_name";

    # don't reload plugins if they are already loaded
    if (!defined &{"${package}::plugin_name"}) {
        for my $dir (@plugin_dirs) {
            next if !-e "$dir/$plugin";
            Qpsmtpd::Plugin->compile($plugin_name, $package,
                                     "$dir/$plugin", $self->{_test_mode},
                                     $plugin);
            if ($safe_line !~ /logging/) {
                $self->log(LOGDEBUG, "Loading $safe_line from $dir/$plugin");
            }
            last;
        }
        if (!defined &{"${package}::plugin_name"}) {
            die "Plugin $plugin_name not found in our plugin dirs (",
              join(', ', @plugin_dirs), ")";
        }
    }

    my $plug = $package->new();
    $plug->_register($self, @args);

    return $plug;
}

sub _load_package_plugin {
    my ($self, $plugin, $plugin_line, $args) = @_;

    # "full" package plugin (My::Plugin)
    my $package = $plugin;
    $package =~ s/[^_a-z0-9:]+//gi;
    my $eval =
      qq[require $package;\n] . qq[sub ${plugin}::plugin_name { '$plugin' }];
    $eval =~ m/(.*)/s;
    $eval = $1;
    eval $eval;    ## no critic (Eval)
    die "Failed loading $package - eval $@" if $@;

    if ($plugin_line !~ /logging/) {
        $self->log(LOGDEBUG, "Loading $package ($plugin_line)");
    }

    my $plug = $package->new();
    $plug->_register($self, @$args);

    return $plug;
}

sub transaction { return {}; }    # base class implements empty transaction

sub run_hooks {
    my ($self, $hook) = (shift, shift);
    if (my @local_hooks = $self->hooks($hook)) {
        $self->{_continuation} = [$hook, [@_], @local_hooks];
        return $self->run_continuation();
    }
    return $self->hook_responder($hook, [0, ''], [@_]);
}

sub run_hooks_no_respond {
    my ($self, $hook) = (shift, shift);
    if (!$hooks->{$hook}) {
        return 0,'';
    }

    my @r;
    for my $code (@{$hooks->{$hook}}) {
        eval { @r = $code->{code}->($self, $self->transaction, @_); };
        if ($@) {
            warn("FATAL PLUGIN ERROR [" . $code->{name} . "]: ", $@);
            next;
        }
        last if $r[0] != DECLINED;
    }
    $r[0] = DECLINED if not defined $r[0];
    return @r;
}

sub run_continuation {
    my $self = shift;

    die "No continuation in progress\n" if !$self->{_continuation};
    my $todo = $self->{_continuation};
    $self->{_continuation} = undef;
    my $hook = shift @$todo or die "No hook in the continuation";
    my $args = shift @$todo or die "No hook args in the continuation";
    my @r;

    while (@$todo) {
        my $code = shift @$todo;
        my $name = $code->{name};

        $self->varlog(LOGDEBUG, $hook, $name);
        my $tran = $self->transaction;
        eval { @r = $code->{code}->($self, $tran, @$args); };
        if ($@) {
            chomp $@;
            $self->log(LOGCRIT, "FATAL PLUGIN ERROR [$name]: ", $@);
            next;
        }

        my $log_msg = "Plugin $name, hook $hook returned ";
        if (!defined $r[0]) {
            $self->log(LOGERROR, $log_msg . "undef!");
            next;
        }
        if ( !return_code($r[0]) ) {
            $self->log(LOGERROR, $log_msg . $r[0]);
            next;
        }

        if ($tran) {
            my $tnotes = $tran->notes($name);
            if (!defined $tnotes || ref $tnotes eq 'HASH') {
                $tnotes->{"hook_$hook"}{return} = $r[0];
            };
        }
        else {
            my $cnotes = $self->connection->notes($name);
            if (!defined $cnotes || ref $cnotes eq 'HASH') {
                $cnotes->{"hook_$hook"}{return} = $r[0];
            };
        }

        if (   $r[0] == DENY
            || $r[0] == DENYSOFT
            || $r[0] == DENY_DISCONNECT
            || $r[0] == DENYSOFT_DISCONNECT)
        {
            $r[1] = '' if !defined $r[1];
            $self->log(LOGDEBUG, $log_msg . return_code($r[0]) . ", $r[1]");
            if ($hook ne 'deny') {
                $self->run_hooks_no_respond('deny', $name, $r[0], $r[1]);
            };
        }
        else {
            $r[1] = '' if not defined $r[1];
            $self->log(LOGDEBUG, $log_msg . return_code($r[0]) . ", $r[1]");
            $self->run_hooks_no_respond('ok', $name, $r[0], $r[1]) if $hook ne 'ok';
        }

        last if $r[0] != DECLINED;
    }
    $r[0] = DECLINED if ! defined $r[0];

    # hook_*_parse() may return a CODE ref..
    # ... which breaks when splitting as string:
    if ('CODE' ne ref $r[1]) {
        @r = map { split /\n/ } @r;
    };
    return $self->hook_responder($hook, \@r, $args);
}

sub hook_responder {
    my ($self, $hook, $msg, $args) = @_;
    my $code = shift @$msg;

    if (my $meth = $self->can($hook . '_respond')) {
        return $meth->($self, $code, $msg, $args);
    }
    return $code, @$msg;
}

sub _register_hook {
    my ($self, $hook, $code, $unshift) = @_;

    if ($unshift) {
        unshift @{$hooks->{$hook}}, $code;
      $@ and warn("FATAL LOGGING PLUGIN ERROR: ", $@) and next;
    }
    else {
      $self->varlog(LOGINFO, $hook, $code->{name});
      eval { (@r) = $code->{code}->($self, $self->transaction, @$args); };
      $@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR: ", $@) and next;

    my $self = shift;

    return $Spool_dir if $Spool_dir;    # already set

    $self->log(LOGDEBUG, "Initializing spool_dir");
    $Spool_dir = $self->config('spool_dir') || $self->tildeexp('~/tmp/');

    $Spool_dir .= "/" if $Spool_dir !~ m!/$!;

    $Spool_dir =~ /^(.+)$/ or die "spool_dir not configured properly";
    $Spool_dir = $1;                    # cleanse the taint

    my $Spool_perms = $self->config('spool_perms') || '0700';

    if (!-d $Spool_dir) {               # create if it doesn't exist
        mkdir($Spool_dir, oct($Spool_perms))
          or die "Could not create spool_dir $Spool_dir: $!";
    }

    # Make sure the spool dir has appropriate rights
    if (((stat $Spool_dir)[2] & oct('07777')) != oct($Spool_perms)) {
        $self->log(LOGWARN,
                   "Permissions on spool_dir $Spool_dir are not $Spool_perms");
    }

    return $Spool_dir;
}

# For unique filenames. We write to a local tmp dir so we don't need
# to make them unpredictable.
my $transaction_counter = 0;

sub temp_file {
    my $self = shift;
    my $filename =
      $self->spool_dir() . join(":", time, $$, $transaction_counter++);
    return $filename;
}

sub temp_dir {
    my ($self, $mask) = @_;
    $mask ||= '0700';
    my $dirname = $self->temp_file();
    if (!-d $dirname) {
        mkdir($dirname, $mask)
            or die "Could not create temporary directory $dirname: $!";
    }
    return $dirname;
}

sub size_threshold {
    my $self = shift;
    return $Size_threshold if defined $Size_threshold;

    $Size_threshold = $self->config('size_threshold') || 0;
    $self->log(LOGDEBUG, "size_threshold set to $Size_threshold");
    return $Size_threshold;
}

sub authenticated {
    my $self = shift;
    return defined $self->{_auth} ? $self->{_auth} : '';
}

sub auth_user {
    my $self = shift;
    return defined $self->{_auth_user} ? $self->{_auth_user} : '';
}

sub auth_mechanism {
    my $self = shift;
    return defined $self->{_auth_mechanism} ? $self->{_auth_mechanism} : '';
}

sub address {
    my $self = shift;
    my $addr = Qpsmtpd::Address->new(@_);
    $addr->qp($self) if $addr;
    return $addr;
}

1;

__END__

=head1 NAME

Qpsmtpd - base class for the qpsmtpd mail server

=head1 DESCRIPTION

This is the base class for the qpsmtpd mail server.  See
L<http://smtpd.develooper.com/> and the I<README> file for more information.

=encoding UTF8

=head1 COPYRIGHT

Copyright 2001-2012 Ask Bjørn Hansen, Develooper LLC.  See the
LICENSE file for more information.

=cut


<MSG> Reduce the log level of the "running plugin" message to LOGDEBUG.
The mere fact that a plugin was called is only useful for debugging.


git-svn-id: 64a18847b464f85e39c77910fd9dc4a9549dd70a@684 958fd67b-6ff1-0310-b445-bb7760255be9

<DFF> @@ -367,7 +367,7 @@ sub run_continuation {
       $@ and warn("FATAL LOGGING PLUGIN ERROR: ", $@) and next;
     }
     else {
-      $self->varlog(LOGINFO, $hook, $code->{name});
+      $self->varlog(LOGDEBUG, $hook, $code->{name});
       eval { (@r) = $code->{code}->($self, $self->transaction, @$args); };
       $@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR: ", $@) and next;
 

{'commit': 'diff --git a/lib/Qpsmtpd.pm b/lib/Qpsmtpd.pm\nindex 2338042..d262518 100644\n--- a/lib/Qpsmtpd.pm\n+++ b/lib/Qpsmtpd.pm\n@@ -367,7 +367,7 @@ sub run_continuation {\n       $@ and warn("FATAL LOGGING PLUGIN ERROR: ", $@) and next;\n     }\n     else {\n-      $self->varlog(LOGINFO, $hook, $code->{name});\n+      $self->varlog(LOGDEBUG, $hook, $code->{name});\n       eval { (@r) = $code->{code}->($self, $self->transaction, @$args); };\n       $@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR: ", $@) and next;\n \n', 'commit_subject': 'Reduce the log level of the "running plugin" message to LOGDEBUG. The mere fact that a plugin was called is only useful for debugging.', 'repo_name': 'smtpd/qpsmtpd', 'committer': {'name': 'Peter J. Holzer', 'email': 'f35b2f1d5a872c871c8d8cb009a962d73a619f3c@hjp.at', 'time_sec': '1166262969', 'tz_offset': '0', 'date': {'seconds': '1166262969'}}, 'commit_tree': '88b3c65c8e825e7797129db31e2fd8e4dcdf50f7', 'commit_message': 'Reduce the log level of the "running plugin" message to LOGDEBUG.\nThe mere fact that a plugin was called is only useful for debugging.\n\n\ngit-svn-id: 64a18847b464f85e39c77910fd9dc4a9549dd70a@684 958fd67b-6ff1-0310-b445-bb7760255be9\n', 'language_name': 'Perl', 'license': 'mit', 'hunks': '@@ -367,7 +367,7 @@ sub run_continuation {\n       $@ and warn("FATAL LOGGING PLUGIN ERROR: ", $@) and next;\n     }\n     else {\n-      $self->varlog(LOGINFO, $hook, $code->{name});\n+      $self->varlog(LOGDEBUG, $hook, $code->{name});\n       eval { (@r) = $code->{code}->($self, $self->transaction, @$args); };\n       $@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR: ", $@) and next;\n \n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/lib/Qpsmtpd.pm', 'tgt_file': 'b/lib/Qpsmtpd.pm', 'file_extension': '.pm', 'before_file': 'package Qpsmtpd;\nuse strict;\n#use warnings;\n\nour $VERSION = \'0.96\';\nuse vars qw($TraceLevel $Spool_dir $Size_threshold);\n\nuse lib \'lib\';\nuse parent \'Qpsmtpd::Base\';\nuse Qpsmtpd::Address;\nuse Qpsmtpd::Config;\nuse Qpsmtpd::Constants;\n\nour $hooks = {};\nour $LOGGING_LOADED = 0;\nmy $git = git_version();\n\nsub _restart {\n    my $self = shift;\n    my %args = @_;\n    if ($args{restart}) {\n\n        # reset all global vars to defaults\n        $self->conf->clear_cache();\n        $hooks          = {};\n        $LOGGING_LOADED = 0;\n        $TraceLevel     = LOGWARN;\n        $Spool_dir      = undef;\n        $Size_threshold = undef;\n    }\n}\n\nsub version { $VERSION . ($git ? "/$git" : \'\') }\n\nsub git_version {\n    return if !-e \'.git\';\n    {\n        local $ENV{PATH} = "/usr/bin:/usr/local/bin:/opt/local/bin/";\n        $git = `git describe --tags`;\n        $git && chomp $git;\n    }\n    return $git;\n}\n\nsub TRACE_LEVEL { $TraceLevel };    # leave for plugin compatibility\n\nsub hooks {\n    my ($self, $hook) = @_;\n    if ($hook) {\n        if (!defined $hooks->{$hook}) { return wantarray ? () : []; };\n        return wantarray ? @{$hooks->{$hook}} : $hooks->{$hook};\n    };\n    return $hooks;\n}\n\nsub load_logging {\n    my $self = shift;\n\n    return if $LOGGING_LOADED;     # already done\n    return if $hooks->{\'logging\'}; # avoid triggering log activity\n\n    my @plugin_dirs = $self->conf->from_file(\'plugin_dirs\');\n    if (!@plugin_dirs) {\n        my ($name) = ($0 =~ m!(.*?)/([^/]+)$!);\n        @plugin_dirs = ("$name/plugins");\n    }\n\n    my @loggers = $self->conf->from_file(\'logging\');\n    for my $logger (@loggers) {\n        $self->_load_plugin($logger, @plugin_dirs);\n        $self->log(LOGINFO, "Loaded $logger");\n    }\n\n    $TraceLevel = $self->conf->from_file(\'loglevel\');\n\n    unless (defined($TraceLevel) && $TraceLevel =~ /^\\d+$/) {\n        $TraceLevel = LOGWARN;    # Default if no loglevel file found.\n    }\n\n    $LOGGING_LOADED = 1;\n\n    return @loggers;\n}\n\nsub trace_level { return $TraceLevel; }\n\nsub init_logger {                 # needed for compatibility\n    shift->trace_level();\n}\n\nsub log {\n    my ($self, $trace, @log) = @_;\n    $self->varlog($trace, join(" ", @log));\n}\n\nsub varlog {\n    my ($self, $trace) = (shift, shift);\n    my ($hook, $plugin, @log);\n    if    ($#_ == 0) { (@log) = @_; }                 # log itself\n    elsif ($#_ == 1) { ($hook, @log) = @_; }          # plus the hook\n    else             { ($hook, $plugin, @log) = @_; } # from a plugin\n\n    $self->load_logging;\n\n    my ($rc) =\n      $self->run_hooks_no_respond("logging", $trace, $hook, $plugin, @log)\n      or return;\n\n    return if $rc == DECLINED || $rc == OK;              # plugin success\n    return if $trace > $TraceLevel;\n\n    # no logging plugins registered, fall back to STDERR\n    my $prefix =\n        defined $plugin && defined $hook ? " ($hook) $plugin:"\n      : defined $plugin ? " $plugin:"\n      : defined $hook   ? " ($hook) running plugin:"\n      :                   \'\';\n\n    warn join(\' \', $$ . $prefix, @log), "\\n";\n}\n\nsub conf {\n    my $self = shift;\n    if (!$self->{_config}) {\n        $self->{_config} = Qpsmtpd::Config->new();\n    }\n    return $self->{_config};\n}\n\nsub config {\n    my $self = shift;\n    return $self->conf->config($self, @_);\n}\n\nsub config_dir {\n    my $self = shift;\n    return $self->conf->config_dir(@_);\n}\n\nsub plugin_dirs {\n    my $self        = shift;\n    my @plugin_dirs = $self->config(\'plugin_dirs\');\n\n    unless (@plugin_dirs) {\n        my ($path) = ($ENV{PROCESS} ? $ENV{PROCESS} : $0) =~ m!(.*?)/([^/]+)$!;\n        @plugin_dirs = ("$path/plugins");\n    }\n    return @plugin_dirs;\n}\n\nsub load_plugins {\n    my $self = shift;\n\n    my @plugins = $self->config(\'plugins\');\n    my @loaded;\n\n    if ($hooks->{queue}) {\n\n        #$self->log(LOGWARN, "Plugins already loaded");\n        return @plugins;\n    }\n\n    for my $plugin_line (@plugins) {\n        my $this_plugin = $self->_load_plugin($plugin_line, $self->plugin_dirs);\n        push @loaded, $this_plugin if $this_plugin;\n    }\n\n    return @loaded;\n}\n\nsub _load_plugin {\n    my ($self, $plugin_line, @plugin_dirs) = @_;\n\n    # untaint the config data before passing it to plugins\n    my ($safe_line) = $plugin_line =~ /^([ -~]+)$/    # all ascii printable\n      or die "unsafe characters in config line: $plugin_line\\n";\n    my ($plugin, @args) = split /\\s+/, $safe_line;\n\n    if ($plugin =~ m/::/) {\n        return $self->_load_package_plugin($plugin, $safe_line, \\@args);\n    }\n\n    # regular plugins/$plugin plugin\n    my $plugin_name = $plugin;\n    $plugin =~ s/:\\d+$//;    # after this point, only used for filename\n\n    # Escape everything into valid perl identifiers\n    $plugin_name =~ s/([^A-Za-z0-9_\\/])/sprintf("_%2x",unpack("C",$1))/eg;\n\n    # second pass cares for slashes and words starting with a digit\n    $plugin_name =~ s{\n    (/+)       # directory\n    (\\d?)      # package\'s first character\n    }[\n        "::" . (length $2 ? sprintf("_%2x",unpack("C",$2)) : \'\')\n    ]egx;\n\n    my $package = "Qpsmtpd::Plugin::$plugin_name";\n\n    # don\'t reload plugins if they are already loaded\n    if (!defined &{"${package}::plugin_name"}) {\n        for my $dir (@plugin_dirs) {\n            next if !-e "$dir/$plugin";\n            Qpsmtpd::Plugin->compile($plugin_name, $package,\n                                     "$dir/$plugin", $self->{_test_mode},\n                                     $plugin);\n            if ($safe_line !~ /logging/) {\n                $self->log(LOGDEBUG, "Loading $safe_line from $dir/$plugin");\n            }\n            last;\n        }\n        if (!defined &{"${package}::plugin_name"}) {\n            die "Plugin $plugin_name not found in our plugin dirs (",\n              join(\', \', @plugin_dirs), ")";\n        }\n    }\n\n    my $plug = $package->new();\n    $plug->_register($self, @args);\n\n    return $plug;\n}\n\nsub _load_package_plugin {\n    my ($self, $plugin, $plugin_line, $args) = @_;\n\n    # "full" package plugin (My::Plugin)\n    my $package = $plugin;\n    $package =~ s/[^_a-z0-9:]+//gi;\n    my $eval =\n      qq[require $package;\\n] . qq[sub ${plugin}::plugin_name { \'$plugin\' }];\n    $eval =~ m/(.*)/s;\n    $eval = $1;\n    eval $eval;    ## no critic (Eval)\n    die "Failed loading $package - eval $@" if $@;\n\n    if ($plugin_line !~ /logging/) {\n        $self->log(LOGDEBUG, "Loading $package ($plugin_line)");\n    }\n\n    my $plug = $package->new();\n    $plug->_register($self, @$args);\n\n    return $plug;\n}\n\nsub transaction { return {}; }    # base class implements empty transaction\n\nsub run_hooks {\n    my ($self, $hook) = (shift, shift);\n    if (my @local_hooks = $self->hooks($hook)) {\n        $self->{_continuation} = [$hook, [@_], @local_hooks];\n        return $self->run_continuation();\n    }\n    return $self->hook_responder($hook, [0, \'\'], [@_]);\n}\n\nsub run_hooks_no_respond {\n    my ($self, $hook) = (shift, shift);\n    if (!$hooks->{$hook}) {\n        return 0,\'\';\n    }\n\n    my @r;\n    for my $code (@{$hooks->{$hook}}) {\n        eval { @r = $code->{code}->($self, $self->transaction, @_); };\n        if ($@) {\n            warn("FATAL PLUGIN ERROR [" . $code->{name} . "]: ", $@);\n            next;\n        }\n        last if $r[0] != DECLINED;\n    }\n    $r[0] = DECLINED if not defined $r[0];\n    return @r;\n}\n\nsub run_continuation {\n    my $self = shift;\n\n    die "No continuation in progress\\n" if !$self->{_continuation};\n    my $todo = $self->{_continuation};\n    $self->{_continuation} = undef;\n    my $hook = shift @$todo or die "No hook in the continuation";\n    my $args = shift @$todo or die "No hook args in the continuation";\n    my @r;\n\n    while (@$todo) {\n        my $code = shift @$todo;\n        my $name = $code->{name};\n\n        $self->varlog(LOGDEBUG, $hook, $name);\n        my $tran = $self->transaction;\n        eval { @r = $code->{code}->($self, $tran, @$args); };\n        if ($@) {\n            chomp $@;\n            $self->log(LOGCRIT, "FATAL PLUGIN ERROR [$name]: ", $@);\n            next;\n        }\n\n        my $log_msg = "Plugin $name, hook $hook returned ";\n        if (!defined $r[0]) {\n            $self->log(LOGERROR, $log_msg . "undef!");\n            next;\n        }\n        if ( !return_code($r[0]) ) {\n            $self->log(LOGERROR, $log_msg . $r[0]);\n            next;\n        }\n\n        if ($tran) {\n            my $tnotes = $tran->notes($name);\n            if (!defined $tnotes || ref $tnotes eq \'HASH\') {\n                $tnotes->{"hook_$hook"}{return} = $r[0];\n            };\n        }\n        else {\n            my $cnotes = $self->connection->notes($name);\n            if (!defined $cnotes || ref $cnotes eq \'HASH\') {\n                $cnotes->{"hook_$hook"}{return} = $r[0];\n            };\n        }\n\n        if (   $r[0] == DENY\n            || $r[0] == DENYSOFT\n            || $r[0] == DENY_DISCONNECT\n            || $r[0] == DENYSOFT_DISCONNECT)\n        {\n            $r[1] = \'\' if !defined $r[1];\n            $self->log(LOGDEBUG, $log_msg . return_code($r[0]) . ", $r[1]");\n            if ($hook ne \'deny\') {\n                $self->run_hooks_no_respond(\'deny\', $name, $r[0], $r[1]);\n            };\n        }\n        else {\n            $r[1] = \'\' if not defined $r[1];\n            $self->log(LOGDEBUG, $log_msg . return_code($r[0]) . ", $r[1]");\n            $self->run_hooks_no_respond(\'ok\', $name, $r[0], $r[1]) if $hook ne \'ok\';\n        }\n\n        last if $r[0] != DECLINED;\n    }\n    $r[0] = DECLINED if ! defined $r[0];\n\n    # hook_*_parse() may return a CODE ref..\n    # ... which breaks when splitting as string:\n    if (\'CODE\' ne ref $r[1]) {\n        @r = map { split /\\n/ } @r;\n    };\n    return $self->hook_responder($hook, \\@r, $args);\n}\n\nsub hook_responder {\n    my ($self, $hook, $msg, $args) = @_;\n    my $code = shift @$msg;\n\n    if (my $meth = $self->can($hook . \'_respond\')) {\n        return $meth->($self, $code, $msg, $args);\n    }\n    return $code, @$msg;\n}\n\nsub _register_hook {\n    my ($self, $hook, $code, $unshift) = @_;\n\n    if ($unshift) {\n        unshift @{$hooks->{$hook}}, $code;\n      $@ and warn("FATAL LOGGING PLUGIN ERROR: ", $@) and next;\n    }\n    else {\n      $self->varlog(LOGINFO, $hook, $code->{name});\n      eval { (@r) = $code->{code}->($self, $self->transaction, @$args); };\n      $@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR: ", $@) and next;\n\n    my $self = shift;\n\n    return $Spool_dir if $Spool_dir;    # already set\n\n    $self->log(LOGDEBUG, "Initializing spool_dir");\n    $Spool_dir = $self->config(\'spool_dir\') || $self->tildeexp(\'~/tmp/\');\n\n    $Spool_dir .= "/" if $Spool_dir !~ m!/$!;\n\n    $Spool_dir =~ /^(.+)$/ or die "spool_dir not configured properly";\n    $Spool_dir = $1;                    # cleanse the taint\n\n    my $Spool_perms = $self->config(\'spool_perms\') || \'0700\';\n\n    if (!-d $Spool_dir) {               # create if it doesn\'t exist\n        mkdir($Spool_dir, oct($Spool_perms))\n          or die "Could not create spool_dir $Spool_dir: $!";\n    }\n\n    # Make sure the spool dir has appropriate rights\n    if (((stat $Spool_dir)[2] & oct(\'07777\')) != oct($Spool_perms)) {\n        $self->log(LOGWARN,\n                   "Permissions on spool_dir $Spool_dir are not $Spool_perms");\n    }\n\n    return $Spool_dir;\n}\n\n# For unique filenames. We write to a local tmp dir so we don\'t need\n# to make them unpredictable.\nmy $transaction_counter = 0;\n\nsub temp_file {\n    my $self = shift;\n    my $filename =\n      $self->spool_dir() . join(":", time, $$, $transaction_counter++);\n    return $filename;\n}\n\nsub temp_dir {\n    my ($self, $mask) = @_;\n    $mask ||= \'0700\';\n    my $dirname = $self->temp_file();\n    if (!-d $dirname) {\n        mkdir($dirname, $mask)\n            or die "Could not create temporary directory $dirname: $!";\n    }\n    return $dirname;\n}\n\nsub size_threshold {\n    my $self = shift;\n    return $Size_threshold if defined $Size_threshold;\n\n    $Size_threshold = $self->config(\'size_threshold\') || 0;\n    $self->log(LOGDEBUG, "size_threshold set to $Size_threshold");\n    return $Size_threshold;\n}\n\nsub authenticated {\n    my $self = shift;\n    return defined $self->{_auth} ? $self->{_auth} : \'\';\n}\n\nsub auth_user {\n    my $self = shift;\n    return defined $self->{_auth_user} ? $self->{_auth_user} : \'\';\n}\n\nsub auth_mechanism {\n    my $self = shift;\n    return defined $self->{_auth_mechanism} ? $self->{_auth_mechanism} : \'\';\n}\n\nsub address {\n    my $self = shift;\n    my $addr = Qpsmtpd::Address->new(@_);\n    $addr->qp($self) if $addr;\n    return $addr;\n}\n\n1;\n\n__END__\n\n=head1 NAME\n\nQpsmtpd - base class for the qpsmtpd mail server\n\n=head1 DESCRIPTION\n\nThis is the base class for the qpsmtpd mail server.  See\nL<http://smtpd.develooper.com/> and the I<README> file for more information.\n\n=encoding UTF8\n\n=head1 COPYRIGHT\n\nCopyright 2001-2012 Ask Bjørn Hansen, Develooper LLC.  See the\nLICENSE file for more information.\n\n=cut\n\n', 'message': 'Reduce the log level of the "running plugin" message to LOGDEBUG.\nThe mere fact that a plugin was called is only useful for debugging.\n\n\ngit-svn-id: 64a18847b464f85e39c77910fd9dc4a9549dd70a@684 958fd67b-6ff1-0310-b445-bb7760255be9\n', 'author_name': 'Peter J. Holzer', 'lang': 'pm'}