diff --git a/spam/CHANGELOG b/spam/CHANGELOG
index bfdc31dff..569a42ee7 100644
--- a/spam/CHANGELOG
+++ b/spam/CHANGELOG
@@ -8,3 +8,5 @@ Updated the setup and procmail delivery pages to allow forwarding of spam to an
Added a Module Config option for specifying a command other than spamassassin (such as spamc) to use in the procmailrc file. This defaults to an automatic mode, where spamc is used if spamd is running.
---- Changes since 1.300 ----
Added Module Config options for commands to run before and after saving SpamAssassin config changes.
+---- Changes since 1.360 ----
+Added the SQL and LDAP Databases page for configuring SpamAssassin to use a MySQL, PostgreSQL or LDAP database for user preferences.
diff --git a/spam/acl_security.pl b/spam/acl_security.pl
index 3bf6146f5..1df871e46 100644
--- a/spam/acl_security.pl
+++ b/spam/acl_security.pl
@@ -8,7 +8,8 @@ sub acl_security_form
print "
$text{'acl_avail'} \n";
print "\n";
local %avail = map { $_, 1 } split(/,/, $_[0]->{'avail'});
-foreach $a ('white', 'score', 'report', 'user', 'header', 'setup', 'procmail') {
+foreach $a ('white', 'score', 'report', 'user', 'header', 'setup', 'procmail',
+ 'db') {
printf "%s\n",
$a, $avail{$a} ? "selected" : "", $text{$a."_title"};
}
diff --git a/spam/defaultacl b/spam/defaultacl
index be978fcd2..e0a08dc75 100644
--- a/spam/defaultacl
+++ b/spam/defaultacl
@@ -1 +1 @@
-avail=white,score,report,user,header,priv,setup,procmail
+avail=white,score,report,user,header,priv,setup,procmail,db
diff --git a/spam/edit_db.cgi b/spam/edit_db.cgi
new file mode 100755
index 000000000..3df68a358
--- /dev/null
+++ b/spam/edit_db.cgi
@@ -0,0 +1,107 @@
+#!/usr/local/bin/perl
+# Show form for SpamAssassin DB options
+
+require './spam-lib.pl';
+&can_use_check("db");
+&ui_print_header(undef, $text{'db_title'}, "");
+$conf = &get_config();
+
+print "$text{'db_desc'}\n";
+&start_form("save_db.cgi", $text{'db_header'});
+
+# Work out backend type
+$dsn = &find_value("user_scores_dsn", $conf);
+if ($dsn =~ /^DBI:([^:]+):([^:]+):([^:]+)(:(\d+))?$/) {
+ # To database
+ $mode = 1;
+ ($dbdriver, $dbdb, $dbhost, $dbport) = ($1, $2, $3, $5);
+ }
+elsif ($dsn =~ /^ldap:\/\/([^:]+)(:(\d+))?\/([^\?]+)\?([^\?]+)\?([^\?]+)\?([^=]+)=__USERNAME__/) {
+ # To LDAP
+ $mode = 3;
+ ($ldaphost, $ldapport, $ldapdn, $ldapattr, $ldapscope, $ldapuid) =
+ ($1, $3, $4, $5, $6, $7);
+ }
+elsif ($dsn) {
+ $mode = 4;
+ }
+else {
+ $mode = 0;
+ }
+
+# Generate input blocks for SQL and LDAP
+$dbtable = &ui_table_start(undef, undef, 2, [ "nowrap" ]);
+$dbtable .= &ui_table_row($text{'db_dbdriver'},
+ &ui_select("dbdriver", $dbdriver || "mysql",
+ [ [ "mysql", "MySQL" ], [ "Pg", "PostgreSQL" ] ],
+ 1, 0, 1));
+$dbtable .= &ui_table_row($text{'db_dbhost'},
+ &ui_textbox("dbhost", $dbhost, 40));
+$dbtable .= &ui_table_row($text{'db_dbdb'},
+ &ui_textbox("dbdb", $dbdb, 40));
+$dbtable .= &ui_table_row($text{'db_dbport'},
+ &ui_opt_textbox("dbport", $dbport, 5, $text{'default'}));
+$dbtable .= &ui_table_end();
+
+$ldaptable = &ui_table_start(undef, undef, 2, [ "nowrap" ]);
+$ldaptable .= &ui_table_row($text{'db_ldaphost'},
+ &ui_textbox("ldaphost", $ldaphost, 40));
+$ldaptable .= &ui_table_row($text{'db_ldapport'},
+ &ui_opt_textbox("ldapport", $ldapport, 5, $text{'default'}));
+$ldaptable .= &ui_table_row($text{'db_ldapdn'},
+ &ui_textbox("ldapdn", $ldapdn, 40));
+$ldaptable .= &ui_table_row($text{'db_ldapattr'},
+ &ui_textbox("ldapattr", $ldapattr, 20));
+$ldaptable .= &ui_table_row($text{'db_ldapscope'},
+ &ui_select("ldapscope", $ldapscope || "sub",
+ [ [ "sub", $text{'db_ldapsub'} ],
+ [ "one", $text{'db_ldapone'} ],
+ [ "base", $text{'db_ldapbase'} ] ], 1, 0, 1));
+$ldaptable .= &ui_table_row($text{'db_ldapuid'},
+ &ui_textbox("ldapuid", $ldapuid || "uid", 20));
+$ldaptable .= &ui_table_end();
+
+
+# Show backend type selector
+print "
$text{'db_dsn'} ";
+print &ui_radio_table("mode", $mode,
+ [ [ 0, $text{'db_mode0'} ],
+ [ 1, $text{'db_mode1'}, $dbtable ],
+ [ 3, $text{'db_mode3'}, $ldaptable ],
+ [ 4, $text{'db_mode4'},
+ &ui_textbox("dsn", $dsn, 60) ] ]);
+print " \n";
+
+print " \n";
+
+# DB login
+print " $text{'db_user'} ";
+$user = &find("user_scores_sql_username", $conf);
+&opt_field("user_scores_sql_username", $user, 20, undef);
+print " \n";
+
+# DB password
+print " $text{'db_pass'} ";
+$pass = &find("user_scores_sql_password", $conf);
+&opt_field("user_scores_sql_password", $pass, 20, undef);
+print " \n";
+
+print " \n";
+
+# LDAP login
+print " $text{'db_luser'} ";
+$user = &find("user_scores_ldap_username", $conf);
+&opt_field("user_scores_ldap_username", $user, 40, undef);
+print " \n";
+
+# LDAP password
+print " $text{'db_lpass'} ";
+$pass = &find("user_scores_ldap_password", $conf);
+&opt_field("user_scores_ldap_password", $pass, 20, undef);
+print " \n";
+
+
+
+&end_form(undef, $text{'save'});
+&ui_print_footer("", $text{'index_return'});
+
diff --git a/spam/images/db.gif b/spam/images/db.gif
new file mode 100644
index 000000000..7de1ebb27
Binary files /dev/null and b/spam/images/db.gif differ
diff --git a/spam/index.cgi b/spam/index.cgi
index 575e6b13d..3e61469a1 100755
--- a/spam/index.cgi
+++ b/spam/index.cgi
@@ -129,6 +129,7 @@ else {
push(@pages, 'razor') if (!$razor && $module_info{'usermin'});
push(@pages, 'setup') if ($spam_enabled == 0);
push(@pages, 'procmail') if ($delivery_enabled == 1);
+ push(@pages, 'db') if (!$module_info{'usermin'});
@pages = grep { &can_use_page($_) } @pages;
$sfolder = $module_info{'usermin'} ? &spam_file_folder()
: undef;
diff --git a/spam/lang/en b/spam/lang/en
index dcb8fb24f..5fe0d735e 100644
--- a/spam/lang/en
+++ b/spam/lang/en
@@ -295,3 +295,38 @@ connect_emysql=Failed to load the database driver $1
connect_elogin=Failed to login to the database $1 : $2.
connect_equery=The database $1 does not contain the preferences table $2
+db_title=SQL and LDAP Databases
+db_header=Configuration storage database options
+db_dsn=Store user configurations in
+db_mode0=Configuration files
+db_mode1=SQL database
+db_mode3=LDAP database
+db_mode4=Other DSN
+db_user=SQL database username
+db_pass=SQL database password
+db_luser=LDAP server username
+db_lpass=LDAP server password
+db_dbdriver=Database type
+db_dbhost=Database server hostname
+db_dbdb=Database name
+db_dbport=Port number
+db_err=Failed to save databases
+db_edbhost=Missing or invalid SQL server hostname
+db_edbdb=Missing or invalid-looking database name
+db_edbport=Missing or invalid SQL server port number
+db_edsn=Missing other DSN
+db_eusername=Missing or invalid database username - no spaces are allowed
+db_ldaphost=LDAP server hostname
+db_ldapport=Port number
+db_ldapdn=Base DN for users
+db_ldapattr=Attribute for SpamAssassin preferences
+db_ldapscope=Search depth
+db_ldapsub=Entire subtree
+db_ldapone=One level
+db_ldapbase=Base only
+db_ldapuid=Attribute for username
+db_eldaphost=Missing or invalid LDAP server hostname
+db_eldapport=Missing or invalid LDAP server port number
+db_eldapdn=Missing or invalid base DN - no spaces are allowed
+db_eldapattr=Missing or invalid SpamAssassin attribute
+db_eldapuid=Missing or invalid username attribute
diff --git a/spam/save_db.cgi b/spam/save_db.cgi
new file mode 100755
index 000000000..e132db862
--- /dev/null
+++ b/spam/save_db.cgi
@@ -0,0 +1,62 @@
+#!/usr/local/bin/perl
+# Save LDAP and SQL database options
+
+require './spam-lib.pl';
+&error_setup($text{'db_err'});
+&can_use_check("db");
+&ReadParse();
+&execute_before("db");
+&lock_spam_files();
+$conf = &get_config();
+
+# Parse backend DSN
+if ($in{'mode'} == 0) {
+ # Files only
+ $dsn = undef;
+ }
+elsif ($in{'mode'} == 1) {
+ # Database of some type
+ gethostbyname($in{'dbhost'}) || &error($text{'db_edbhost'});
+ $in{'dbdb'} =~ /^[a-z0-9\.\-\_]+$/ || &error($text{'db_edbdb'});
+ $in{'dbport_def'} || $in{'dbport'} =~ /^\d+$/ ||
+ &error($text{'db_edbport'});
+ $dsn = join(":", "DBI", $in{'dbdriver'}, $in{'dbdb'}, $in{'dbhost'});
+ $dsn .= ":".$in{'dbport'} if (!$in{'dbport_def'});
+ }
+elsif ($in{'mode'} == 3) {
+ # LDAP
+ gethostbyname($in{'ldaphost'}) || &error($text{'db_eldaphost'});
+ $in{'ldapport_def'} || $in{'ldapport'} =~ /^\d+$/ ||
+ &error($text{'db_eldapport'});
+ $in{'ldapdn'} =~ /^\S+$/ || &error($text{'db_eldapdn'});
+ $in{'ldapattr'} =~ /^\S+$/ || &error($text{'db_eldapattr'});
+ $in{'ldapuid'} =~ /^\S+$/ || &error($text{'db_eldapuid'});
+ $dsn = "ldap://".$in{'ldaphost'}.
+ ($in{'ldapport_def'} ? "" : ":".$in{'ldapport'})."/".
+ $in{'ldapdn'}."?".$in{'ldapattr'}."?".$in{'ldapscope'}."?".
+ $in{'ldapuid'}."=__USERNAME__";
+ }
+else {
+ # Other DSN
+ $in{'dsn'} =~ /\S/ || &error($text{'db_edsn'});
+ $dsn = $in{'dsn'};
+ }
+&save_directives($conf, "user_scores_dsn", [ $dsn ], 1);
+
+# Parse username and password
+&parse_opt($conf, "user_scores_sql_username", \&username_check);
+&parse_opt($conf, "user_scores_sql_password");
+&parse_opt($conf, "user_scores_ldap_username", \&username_check);
+&parse_opt($conf, "user_scores_ldap_password");
+
+&flush_file_lines();
+&unlock_spam_files();
+&execute_after("db");
+&webmin_log("db");
+&redirect("");
+
+sub username_check
+{
+return $_[0] =~ /^\S+$/ ? undef : $text{'db_eusername'};
+}
+
diff --git a/spam/spam-lib.pl b/spam/spam-lib.pl
index 8a58be16e..36213304f 100644
--- a/spam/spam-lib.pl
+++ b/spam/spam-lib.pl
@@ -20,7 +20,7 @@ if ($module_info{'usermin'}) {
}
}
$database_userpref_name = $remote_user;
- $include_config_files = 1; # XXX
+ $include_config_files = $config{'readfiles'};
$add_to_db = 1;
}
else {
@@ -40,12 +40,13 @@ $add_cf = !-d $local_cf ? $local_cf :
$module_info{'usermin'} ? "$local_cf/user_prefs" :
"$local_cf/local.cf";
-# get_config([file])
+# get_config([file], [for-global])
# Return a structure containing the contents of the spamassassin config file
sub get_config
{
+local $forglobal = $_[1];
local @rv;
-if ($include_config_files) {
+if ($include_config_files || $forglobal) {
# Reading from file(s)
local $lnum = 0;
local $file = $_[0] || $local_cf;
@@ -91,10 +92,12 @@ if ($config{'mode'} == 1 || $config{'mode'} == 2) {
local $dbh = &connect_spamassasin_db();
&error($dbh) if (!ref($dbh));
local $cmd = $dbh->prepare("select preference,value from userpref where username = ?");
- $cmd->execute($database_userpref_name);
+ $cmd->execute(!$forglobal ? $database_userpref_name :
+ $config{'dbglobal'} ? $config{'dbglobal'} : '@GLOBAL');
while(my ($name, $value) = $cmd->fetchrow()) {
local $dir = { 'name' => $name,
'value' => $value,
+ 'index' => scalar(@rv),
'mode' => $config{'mode'} };
$dir->{'words'} =
[ split(/\s+/, $dir->{'value'}) ];
@@ -210,6 +213,7 @@ for($i=0; $i<@old || $i<@new; $i++) {
elsif ($new[$i]) {
# Adding a directive
local $addmode = scalar(@old) ? $old[0]->{'mode'} :
+ $new[$i]->{'name'} =~ /^user_scores_/ ? 0 :
$add_to_db ? $config{'mode'} : 0;
if ($addmode == 0) {
# To a file
@@ -231,6 +235,7 @@ for($i=0; $i<@old || $i<@new; $i++) {
# To LDAP
# XXX
}
+ $new[$i]->{'mode'} = $addmode;
$new[$i]->{'index'} = @{$_[0]};
push(@{$_[0]}, $new[$i]);
}
@@ -430,8 +435,10 @@ else {
sub find_default
{
if ($config{'global_cf'}) {
- local $gconf = &get_config($config{'global_cf'});
- local $v = &find_value($_[0], $gconf);
+ if (!defined($global_config_cache)) {
+ $global_config_cache = &get_config($config{'global_cf'}, 1);
+ }
+ local $v = &find_value($_[0], $global_config_cache);
return $v if (defined($v));
}
return $_[1];