From c78b5f4fdc3afab7451f3584660e7d88b5ec5d33 Mon Sep 17 00:00:00 2001 From: Andrea Dell'Amico Date: Sat, 4 Apr 2020 20:13:34 +0200 Subject: [PATCH] spamassassin: installation and configuration of the spamd service and spamass-milter. Support user preferences in postgreSQL. --- README.md | 13 +- defaults/main.yml | 69 +++++++ files/awl_pg.sql | 26 +++ files/bayes_pg.sql | 119 ++++++++++++ files/spamassassin-pg-3.4.sql | 179 ++++++++++++++++++ files/txrep_pg.sql | 27 +++ files/userpref_pg.sql | 7 + handlers/main.yml | 9 + meta/main.yml | 16 ++ tasks/main.yml | 5 + tasks/spamass-milter.yml | 27 +++ tasks/spamassassin.yml | 97 ++++++++++ templates/letsencrypt-spamassassin-hook.sh.j2 | 35 ++++ templates/spamass-milter_sysconfig.j2 | 8 + templates/spamassassin-db.cf.j2 | 19 ++ templates/spamassassin-local.cf.j2 | 19 ++ templates/spamassassin_sysconfig.j2 | 2 + 17 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 defaults/main.yml create mode 100644 files/awl_pg.sql create mode 100644 files/bayes_pg.sql create mode 100644 files/spamassassin-pg-3.4.sql create mode 100644 files/txrep_pg.sql create mode 100644 files/userpref_pg.sql create mode 100644 handlers/main.yml create mode 100644 meta/main.yml create mode 100644 tasks/main.yml create mode 100644 tasks/spamass-milter.yml create mode 100644 tasks/spamassassin.yml create mode 100644 templates/letsencrypt-spamassassin-hook.sh.j2 create mode 100644 templates/spamass-milter_sysconfig.j2 create mode 100644 templates/spamassassin-db.cf.j2 create mode 100644 templates/spamassassin-local.cf.j2 create mode 100644 templates/spamassassin_sysconfig.j2 diff --git a/README.md b/README.md index ad965d8..f05d3a9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ # ansible-role-spamassassin -Install and configures spamassassin, https://spamassassin.apache.org \ No newline at end of file +Installs and configures spamassassin, + +* We actually support PostgreSQL as remote backend +* The sql present in the `files` directory is valid on spamassassin 3.4 + +## TODO + +* Clean the txrep stale data regulary, running the following query (PostgreSQL) + +``` sql +DELETE FROM txrep WHERE last_hit <= (now() - INTERVAL '120 day'); +``` diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..0750e8e --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,69 @@ +--- +spamassassin_install: True +spamassassin_spamd_enabled: True +spamassassin_sql_backend: False +spamassassin_required_hits: 5 +spamassassin_report_safe: 0 +spamassassin_rewrite_subject: "[SPAM]" +spamassassin_user: spamassassin +spamassassin_group: '{{ spamassassin_user }}' +spamassassin_home: /etc/mail/spamassassin + +spamassassin_rh_packages: + - spamassassin + - spamassassin-iXhash2 + +spamassassin_sql_rh_packages: + - perl-DBD-Pg + - perl-DBI + +spamassassin_spamd_port: 783 +spamassassin_conf_dir: '/etc/mail/spamassassin' +spamassassin_sysconfig_file: '/etc/sysconfig/spamassassin' +# Only postgresql support for the time being +spamassassin_db_user_config: True +spamassassin_spamd_sql_opts: '-q -x ' +spamassassin_db_name: 'spamassassin' +spamassassin_db_user: 'spamassassin_u' +# spamassassin_db_pwd: 'use a vault file' +spamassassin_db_host: 'localhost' +spamassassin_db_external_host: '{{ spamassassin_db_host }}' +spamassassin_db_port: 5432 +spamassassin_db_sql_file: 'spamassassin-pg-3.4.sql' +spamassassin_db_allowed_hosts: + - '127.0.0.1' + - '{{ ansible_fqdn }}' + +spamassassin_use_bayes: False +spamassassin_bayes_sql_db: '{{ spamassassin_db_user_config }}' +spamassassin_use_bayes_autolearn: '0' +spamassassin_use_bayes_auto_expire: '1' +spamassassin_auto_whitelist: False +spamassassin_auto_whitelist_sql_db: '{{ spamassassin_db_user_config }}' + +spamassassin_spamd_ssl_enabled: True +spamassassin_spamd_ssl_opts: '-u {{ spamassassin_user }} -g {{ spamassassin_group }} --ssl --server-key {{ spamassassin_home }}/client-key.pem --server-cert {{ spamassassin_home }}/client-cert.pem' + +psql_db_data: + - { name: '{{ spamassassin_db_name }}', encoding: 'UTF8', user: '{{ spamassassin_db_user }}', roles: 'NOCREATEDB,NOSUPERUSER', pwd: '{{ spamassassin_db_pwd }}', managedb: True, allowed_hosts: '{{ spamassassin_db_allowed_hosts }}', extensions: [ '' ], schema_file: '/srv/spamassassin.sql' } + +## Spamassassin milter settings +spamassassin_milter_install: False +spamassassin_rh_milter_packages: + - spamass-milter + - spamass-milter-postfix + +spamassassin_milter_set_pref_dom_and_user: False +spamassassin_milter_pref_default_domain: 'localhost' +spamassassin_milter_pref_default_user: 'root' +spamassassin_milter_reject_code: '5.7.1' +spamassassin_milter_reject_message: 'Blocked by SpamAssassin' +spamassassin_milter_connect_to_external_spamd: False +spamassassin_milter_external_spamd_host: '127.0.0.1' +spamassassin_milter_external_spamd_port: '{{ spamassassin_spamd_port }}' +spamassassin_milter_exclude_whitelisted_networks: False +# Separate with commas +spamassassin_milter_whitelisted_networks: '127.0.0.1/8' +spamassassin_milter_reject_limit: 15 +spamassassin_milter_change_headers: True + diff --git a/files/awl_pg.sql b/files/awl_pg.sql new file mode 100644 index 0000000..2cb3f3e --- /dev/null +++ b/files/awl_pg.sql @@ -0,0 +1,26 @@ +CREATE TABLE awl ( + username varchar(100) NOT NULL default '', + email varchar(255) NOT NULL default '', + ip varchar(40) NOT NULL default '', + msgcount bigint NOT NULL default '0', + totscore float NOT NULL default '0', + signedby varchar(255) NOT NULL default '', + last_hit timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (username,email,signedby,ip) +); + +create index awl_last_hit on awl (last_hit); + +create OR REPLACE function update_awl_last_hit() +RETURNS TRIGGER AS $$ +BEGIN + NEW.last_hit = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +create TRIGGER update_awl_update_last_hit BEFORE UPDATE +ON awl FOR EACH ROW EXECUTE PROCEDURE +update_awl_last_hit(); + +ALTER TABLE awl SET (fillfactor=95); diff --git a/files/bayes_pg.sql b/files/bayes_pg.sql new file mode 100644 index 0000000..ef96472 --- /dev/null +++ b/files/bayes_pg.sql @@ -0,0 +1,119 @@ + +CREATE TABLE bayes_expire ( + id integer NOT NULL default '0', + runtime integer NOT NULL default '0' +) WITHOUT OIDS; + +CREATE INDEX bayes_expire_idx1 ON bayes_expire (id); + +CREATE TABLE bayes_global_vars ( + variable varchar(30) NOT NULL default '', + value varchar(200) NOT NULL default '', + PRIMARY KEY (variable) +) WITHOUT OIDS; + +INSERT INTO bayes_global_vars VALUES ('VERSION','3'); + +CREATE TABLE bayes_seen ( + id integer NOT NULL default '0', + msgid varchar(200) NOT NULL default '', + flag character(1) NOT NULL default '', + PRIMARY KEY (id,msgid) +) WITHOUT OIDS; + +CREATE TABLE bayes_token ( + id integer NOT NULL default '0', + token bytea NOT NULL default '', + spam_count integer NOT NULL default '0', + ham_count integer NOT NULL default '0', + atime integer NOT NULL default '0', + PRIMARY KEY (id,token) +) WITHOUT OIDS; + +CREATE INDEX bayes_token_idx1 ON bayes_token (token); + +ALTER TABLE bayes_token SET (fillfactor=95); + +CREATE TABLE bayes_vars ( + id serial NOT NULL, + username varchar(200) NOT NULL default '', + spam_count integer NOT NULL default '0', + ham_count integer NOT NULL default '0', + token_count integer NOT NULL default '0', + last_expire integer NOT NULL default '0', + last_atime_delta integer NOT NULL default '0', + last_expire_reduce integer NOT NULL default '0', + oldest_token_age integer NOT NULL default '2147483647', + newest_token_age integer NOT NULL default '0', + PRIMARY KEY (id) +) WITHOUT OIDS; + +CREATE UNIQUE INDEX bayes_vars_idx1 ON bayes_vars (username); + +CREATE OR REPLACE FUNCTION greatest_int (integer, integer) + RETURNS INTEGER + IMMUTABLE STRICT + AS 'SELECT CASE WHEN $1 < $2 THEN $2 ELSE $1 END;' + LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION least_int (integer, integer) + RETURNS INTEGER + IMMUTABLE STRICT + AS 'SELECT CASE WHEN $1 < $2 THEN $1 ELSE $2 END;' + LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION put_tokens(INTEGER, + BYTEA[], + INTEGER, + INTEGER, + INTEGER) +RETURNS VOID AS ' +DECLARE + inuserid ALIAS FOR $1; + intokenary ALIAS FOR $2; + inspam_count ALIAS FOR $3; + inham_count ALIAS FOR $4; + inatime ALIAS FOR $5; + _token BYTEA; + new_tokens INTEGER := 0; +BEGIN + for i in array_lower(intokenary, 1) .. array_upper(intokenary, 1) + LOOP + _token := intokenary[i]; + UPDATE bayes_token + SET spam_count = greatest_int(spam_count + inspam_count, 0), + ham_count = greatest_int(ham_count + inham_count, 0), + atime = greatest_int(atime, inatime) + WHERE id = inuserid + AND token = _token; + IF NOT FOUND THEN + -- we do not insert negative counts, just return true + IF NOT (inspam_count < 0 OR inham_count < 0) THEN + INSERT INTO bayes_token (id, token, spam_count, ham_count, atime) + VALUES (inuserid, _token, inspam_count, inham_count, inatime); + IF FOUND THEN + new_tokens := new_tokens + 1; + END IF; + END IF; + END IF; + END LOOP; + + IF new_tokens > 0 AND inatime > 0 THEN + UPDATE bayes_vars + SET token_count = token_count + new_tokens, + newest_token_age = greatest_int(newest_token_age, inatime), + oldest_token_age = least_int(oldest_token_age, inatime) + WHERE id = inuserid; + ELSIF new_tokens > 0 AND NOT inatime > 0 THEN + UPDATE bayes_vars + SET token_count = token_count + new_tokens + WHERE id = inuserid; + ELSIF NOT new_tokens > 0 AND inatime > 0 THEN + UPDATE bayes_vars + SET newest_token_age = greatest_int(newest_token_age, inatime), + oldest_token_age = least_int(oldest_token_age, inatime) + WHERE id = inuserid; + END IF; + RETURN; +END; +' LANGUAGE 'plpgsql'; diff --git a/files/spamassassin-pg-3.4.sql b/files/spamassassin-pg-3.4.sql new file mode 100644 index 0000000..d7b90a1 --- /dev/null +++ b/files/spamassassin-pg-3.4.sql @@ -0,0 +1,179 @@ +CREATE TABLE userpref ( + prefid bigserial NOT NULL unique primary key, + username varchar(100) NOT NULL, + preference varchar(50) NOT NULL, + value varchar(100) NOT NULL +); +CREATE INDEX userpref_username_idx ON userpref(username); +CREATE TABLE awl ( + username varchar(100) NOT NULL default '', + email varchar(255) NOT NULL default '', + ip varchar(40) NOT NULL default '', + msgcount bigint NOT NULL default '0', + totscore float NOT NULL default '0', + signedby varchar(255) NOT NULL default '', + last_hit timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (username,email,signedby,ip) +); + +create index awl_last_hit on awl (last_hit); + +create OR REPLACE function update_awl_last_hit() +RETURNS TRIGGER AS $$ +BEGIN + NEW.last_hit = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +create TRIGGER update_awl_update_last_hit BEFORE UPDATE +ON awl FOR EACH ROW EXECUTE PROCEDURE +update_awl_last_hit(); + +ALTER TABLE awl SET (fillfactor=95); + +CREATE TABLE bayes_expire ( + id integer NOT NULL default '0', + runtime integer NOT NULL default '0' +) WITHOUT OIDS; + +CREATE INDEX bayes_expire_idx1 ON bayes_expire (id); + +CREATE TABLE bayes_global_vars ( + variable varchar(30) NOT NULL default '', + value varchar(200) NOT NULL default '', + PRIMARY KEY (variable) +) WITHOUT OIDS; + +INSERT INTO bayes_global_vars VALUES ('VERSION','3'); + +CREATE TABLE bayes_seen ( + id integer NOT NULL default '0', + msgid varchar(200) NOT NULL default '', + flag character(1) NOT NULL default '', + PRIMARY KEY (id,msgid) +) WITHOUT OIDS; + +CREATE TABLE bayes_token ( + id integer NOT NULL default '0', + token bytea NOT NULL default '', + spam_count integer NOT NULL default '0', + ham_count integer NOT NULL default '0', + atime integer NOT NULL default '0', + PRIMARY KEY (id,token) +) WITHOUT OIDS; + +CREATE INDEX bayes_token_idx1 ON bayes_token (token); + +ALTER TABLE bayes_token SET (fillfactor=95); + +CREATE TABLE bayes_vars ( + id serial NOT NULL, + username varchar(200) NOT NULL default '', + spam_count integer NOT NULL default '0', + ham_count integer NOT NULL default '0', + token_count integer NOT NULL default '0', + last_expire integer NOT NULL default '0', + last_atime_delta integer NOT NULL default '0', + last_expire_reduce integer NOT NULL default '0', + oldest_token_age integer NOT NULL default '2147483647', + newest_token_age integer NOT NULL default '0', + PRIMARY KEY (id) +) WITHOUT OIDS; + +CREATE UNIQUE INDEX bayes_vars_idx1 ON bayes_vars (username); + +CREATE OR REPLACE FUNCTION greatest_int (integer, integer) + RETURNS INTEGER + IMMUTABLE STRICT + AS 'SELECT CASE WHEN $1 < $2 THEN $2 ELSE $1 END;' + LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION least_int (integer, integer) + RETURNS INTEGER + IMMUTABLE STRICT + AS 'SELECT CASE WHEN $1 < $2 THEN $1 ELSE $2 END;' + LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION put_tokens(INTEGER, + BYTEA[], + INTEGER, + INTEGER, + INTEGER) +RETURNS VOID AS ' +DECLARE + inuserid ALIAS FOR $1; + intokenary ALIAS FOR $2; + inspam_count ALIAS FOR $3; + inham_count ALIAS FOR $4; + inatime ALIAS FOR $5; + _token BYTEA; + new_tokens INTEGER := 0; +BEGIN + for i in array_lower(intokenary, 1) .. array_upper(intokenary, 1) + LOOP + _token := intokenary[i]; + UPDATE bayes_token + SET spam_count = greatest_int(spam_count + inspam_count, 0), + ham_count = greatest_int(ham_count + inham_count, 0), + atime = greatest_int(atime, inatime) + WHERE id = inuserid + AND token = _token; + IF NOT FOUND THEN + -- we do not insert negative counts, just return true + IF NOT (inspam_count < 0 OR inham_count < 0) THEN + INSERT INTO bayes_token (id, token, spam_count, ham_count, atime) + VALUES (inuserid, _token, inspam_count, inham_count, inatime); + IF FOUND THEN + new_tokens := new_tokens + 1; + END IF; + END IF; + END IF; + END LOOP; + + IF new_tokens > 0 AND inatime > 0 THEN + UPDATE bayes_vars + SET token_count = token_count + new_tokens, + newest_token_age = greatest_int(newest_token_age, inatime), + oldest_token_age = least_int(oldest_token_age, inatime) + WHERE id = inuserid; + ELSIF new_tokens > 0 AND NOT inatime > 0 THEN + UPDATE bayes_vars + SET token_count = token_count + new_tokens + WHERE id = inuserid; + ELSIF NOT new_tokens > 0 AND inatime > 0 THEN + UPDATE bayes_vars + SET newest_token_age = greatest_int(newest_token_age, inatime), + oldest_token_age = least_int(oldest_token_age, inatime) + WHERE id = inuserid; + END IF; + RETURN; +END; +' LANGUAGE 'plpgsql'; +CREATE TABLE txrep ( + username varchar(100) NOT NULL default '', + email varchar(255) NOT NULL default '', + ip varchar(40) NOT NULL default '', + msgcount bigint NOT NULL default '0', + totscore float NOT NULL default '0', + signedby varchar(255) NOT NULL default '', + last_hit timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (username,email,signedby,ip) +); + +create index txrep_last_hit on txrep (last_hit); + +create OR REPLACE function update_txrep_last_hit() +RETURNS TRIGGER AS $$ +BEGIN + NEW.last_hit = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +create TRIGGER update_txrep_update_last_hit BEFORE UPDATE +ON txrep FOR EACH ROW EXECUTE PROCEDURE +update_txrep_last_hit(); + +ALTER TABLE txrep SET (fillfactor=95); + diff --git a/files/txrep_pg.sql b/files/txrep_pg.sql new file mode 100644 index 0000000..191074c --- /dev/null +++ b/files/txrep_pg.sql @@ -0,0 +1,27 @@ +CREATE TABLE txrep ( + username varchar(100) NOT NULL default '', + email varchar(255) NOT NULL default '', + ip varchar(40) NOT NULL default '', + msgcount bigint NOT NULL default '0', + totscore float NOT NULL default '0', + signedby varchar(255) NOT NULL default '', + last_hit timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (username,email,signedby,ip) +); + +create index txrep_last_hit on txrep (last_hit); + +create OR REPLACE function update_txrep_last_hit() +RETURNS TRIGGER AS $$ +BEGIN + NEW.last_hit = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +create TRIGGER update_txrep_update_last_hit BEFORE UPDATE +ON txrep FOR EACH ROW EXECUTE PROCEDURE +update_txrep_last_hit(); + +ALTER TABLE txrep SET (fillfactor=95); + diff --git a/files/userpref_pg.sql b/files/userpref_pg.sql new file mode 100644 index 0000000..a50b6f3 --- /dev/null +++ b/files/userpref_pg.sql @@ -0,0 +1,7 @@ +CREATE TABLE userpref ( + prefid bigserial NOT NULL unique primary key, + username varchar(100) NOT NULL, + preference varchar(50) NOT NULL, + value varchar(100) NOT NULL +); +CREATE INDEX userpref_username_idx ON userpref(username); diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..dd29e5d --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Reload spamassassin + service: name=spamassassin state=restarted + +- name: Restart spamassassin + service: name=spamassassin state=restarted + +- name: Restart spamass-milter + service: name=spamass-milter state=restarted diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..d48c856 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,16 @@ +--- +galaxy_info: + author: adellam + description: SpamAssassin 3 installation and configuration + company: ISTI-CNR + license: license (EUPL) + min_ansible_version: 2.7 + platforms: + - name: EL + versions: + - 7 + galaxy_tags: + - mail + - antispam + +dependencies: [] diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..7f54952 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,5 @@ +--- +- import_tasks: spamassassin.yml + when: spamassassin_install | bool +- import_tasks: spamass-milter.yml + when: spamassassin_milter_install | bool diff --git a/tasks/spamass-milter.yml b/tasks/spamass-milter.yml new file mode 100644 index 0000000..010f3ae --- /dev/null +++ b/tasks/spamass-milter.yml @@ -0,0 +1,27 @@ +--- +- name: Install the Spamassassin milter packages + block: + - name: spamassassin milter packages, RH/CentOS + yum: pkg={{ spamassassin_rh_milter_packages }} state=present + + when: ansible_distribution_file_variety == "RedHat" + tags: [ 'spamassassin', 'spamass_milter' ] + +- name: Manage the spamassassin milter service + block: + - name: Install the spamassassin milter startup options + template: src=spamass-milter_sysconfig.j2 dest=/etc/sysconfig/spamass-milter owner=root group=root mode=0444 + notify: Restart spamass-milter + + - name: Ensure that the spamassassin service is started and enabled + service: name=spamass-milter state=started enabled=yes + + tags: [ 'spamassassin', 'spamassassin_conf', 'spamassassin_service' ] + +- name: Manage the spamassassin service + block: + - name: Shut down the spamassassin service if it is meant to be remote + service: name=spamassassin state=stopped enabled=no + + when: not spamassassin_install | bool + tags: [ 'spamassassin', 'spamassassin_conf', 'spamassassin_service' ] diff --git a/tasks/spamassassin.yml b/tasks/spamassassin.yml new file mode 100644 index 0000000..1d271ec --- /dev/null +++ b/tasks/spamassassin.yml @@ -0,0 +1,97 @@ +--- +- name: Create the spamassasin user and install the Spamassassin packages + block: + - name: Create the spamassassin user + user: name={{ spamassassin_user }} home={{ spamassassin_home }} comment="Spamassassin Service Account" createhome=no shell=/usr/sbin/nologin system=yes + + - name: spamassassin packages, RH/CentOS + yum: pkg={{ spamassassin_rh_packages }} state=present + + - name: spamassassin perl DB* packages, RH/CentOS + yum: pkg={{ spamassassin_sql_rh_packages }} state=present + when: spamassassin_db_user_config | bool + + - name: Set some SELinux booleans related to spamassassin + seboolean: name={{ item }} state=yes persistent=yes + with_items: + - 'spamassassin_can_network' + - 'spamd_update_can_network' + + when: ansible_distribution_file_variety == "RedHat" + tags: [ 'spamassassin' ] + +- name: Manage the letsencrypt configuration + block: + - name: Check if the letsencrypt certificates are in place + stat: path={{ letsencrypt_acme_certs_dir }}/privkey + register: letsencrypt_keyfile + + - name: Copy the letsencrypt certificate key into the right place + copy: src={{ letsencrypt_acme_certs_dir }}/privkey dest={{ spamassassin_home }}/client-key.pem owner={{ spamassassin_user }} group={{ spamassassin_group }} mode=0400 remote_src=yes force=yes + when: letsencrypt_keyfile.stat.exists is defined and letsencrypt_keyfile.stat.exists | bool + notify: Restart spamassassin + + - name: Copy the letsencrypt public certificate into the right place + copy: src={{ letsencrypt_acme_certs_dir }}/fullchain dest={{ spamassassin_home }}/client-cert.pem owner={{ spamassassin_user }} group={{ spamassassin_group }} mode=0444 remote_src=yes force=yes + when: letsencrypt_keyfile.stat.exists is defined and letsencrypt_keyfile.stat.exists | bool + notify: Restart spamassassin + + - name: Create the acme hooks directory if it does not yet exist + file: dest={{ letsencrypt_acme_sh_services_scripts_dir }} state=directory owner=root group=root + + - name: Install a script that fix the letsencrypt certificate for mysql and then reload the service + template: src=letsencrypt-spamassassin-hook.sh.j2 dest={{ letsencrypt_acme_sh_services_scripts_dir }}/spamassassin owner=root group=root mode=4555 + + when: + - letsencrypt_acme_sh_install is defined and letsencrypt_acme_sh_install | bool + - spamassassin_spamd_ssl_enabled | bool + tags: [ 'spamassassin', 'letsencrypt', 'spamassassin_letsencrypt' ] + +- name: Install the Spamassassin base configuration + block: + - name: spamassassin local config + template: src=spamassassin-local.cf.j2 dest={{ spamassassin_conf_dir }}/local.cf owner=root group={{ spamassassin_group }} mode=0440 + notify: Reload spamassassin + + - name: spamassassin spamd defaults + template: src=spamassassin_sysconfig.j2 dest=/etc/sysconfig/spamassassin owner=root group=root mode=0444 + notify: Reload spamassassin + + tags: [ 'spamassassin', 'spamassassin_conf' ] + +- name: Install the Spamassassin DB configuration + block: + - name: spamassassin db config + template: src=spamassassin-db.cf.j2 dest={{ spamassassin_conf_dir }}/db.cf owner=root group={{ spamassassin_group }} mode=0440 + notify: Reload spamassassin + + when: spamassassin_db_user_config | bool + tags: [ 'spamassassin', 'spamassassin_conf' ] + +- name: Install the Spamassassin DB configuration + block: + - name: Copy the spamassassin postgresql sql schema files + copy: src={{ spamassassin_db_sql_file }} dest={{ item.schema_file }} force=no + with_items: '{{ psql_db_data }}' + register: pdns_schema + when: item.schema_file is defined + + - name: Install the spamassassin schema file + postgresql_db: name={{ item.name }} login_host='localhost' login_user={{ item.user }} login_password={{ item.pwd }} state=restore target={{ item.schema_file }} port={{ psql_db_port }} + with_items: '{{ psql_db_data }}' + when: + - pdns_schema is changed + - item.schema_file is defined + + delegate_to: '{{ spamassassin_db_external_host }}' + run_once: True + when: spamassassin_db_user_config | bool + tags: [ 'spamassassin', 'spamassassin_conf' ] + +- name: Manage the spamassassin service + block: + - name: Ensure that the spamassassin service is started and enabled + service: name=spamassassin state=started enabled=yes + + when: spamassassin_install | bool + tags: [ 'spamassassin', 'spamassassin_conf', 'spamassassin_service' ] diff --git a/templates/letsencrypt-spamassassin-hook.sh.j2 b/templates/letsencrypt-spamassassin-hook.sh.j2 new file mode 100644 index 0000000..5f87cd0 --- /dev/null +++ b/templates/letsencrypt-spamassassin-hook.sh.j2 @@ -0,0 +1,35 @@ +#!/bin/bash + +LE_SERVICES_SCRIPT_DIR=/usr/lib/acme/hooks +LE_CERTS_DIR={{ letsencrypt_acme_certs_dir }} +LE_LOG_DIR=/var/log/acme +LE_LOG_FILE="$LE_LOG_DIR"/spamassassin.log +SPAMASSASSIN_CERTDIR={{ spamassassin_home }} +RETVAL= + +DATE=$( date ) + +echo "$DATE" >> "$LE_LOG_FILE" + +echo "Check if the certificate changed" >> "$LE_LOG_FILE" +diff "${LE_CERTS_DIR}/fullchain" "$SPAMASSASSIN_CERTDIR"/client-cert.pem > /dev/null +RETVAL=$? +if [ $RETVAL -eq 0 ] ; then + echo "Certificate did not change, exiting." >> "$LE_LOG_FILE" + exit 0 +fi + +echo "Copying the new certificate files" >> "$LE_LOG_FILE" +cp -u "${LE_CERTS_DIR}/fullchain" "$SPAMASSASSIN_CERTDIR"/client-cert.pem +cp -u "${LE_CERTS_DIR}/privkey" "$SPAMASSASSIN_CERTDIR"/client-key.pem +chmod 444 "$SPAMASSASSIN_CERTDIR"/client-cert.pem +chown {{ spamassassin_user }}:{{ spamassassin_group }} "$SPAMASSASSIN_CERTDIR"/client-cert.pem +chmod 400 "$SPAMASSASSIN_CERTDIR"/client-key.pem +chown {{ spamassassin_user }}:{{ spamassassin_group }} "$SPAMASSASSIN_CERTDIR"/client-key.pem + +echo "Restart the spamassassin service" >> "$LE_LOG_FILE" +if [ -x /bin/systemctl ] ; then + systemctl restart spamassassin >> "$LE_LOG_FILE" 2>&1 +else + service spamassassin restart >> "$LE_LOG_FILE" 2>&1 +fi diff --git a/templates/spamass-milter_sysconfig.j2 b/templates/spamass-milter_sysconfig.j2 new file mode 100644 index 0000000..1ba6c07 --- /dev/null +++ b/templates/spamass-milter_sysconfig.j2 @@ -0,0 +1,8 @@ +### You may add configuration parameters here, see spamass-milter(1) +### +### Note that the -x option for expanding aliases and virtusertable entries +### only works if spamass-milter is run as root; you will need to use +### spamass-milter-root.service instead of spamass-milter.service if you +### wish to do this but otherwise it's best to run as the unprivileged user +### sa-milt by using the normal spamass-milter.service +EXTRA_FLAGS="{% if not spamassassin_milter_change_headers %}-m {% endif %}{% if spamassassin_milter_set_pref_dom_and_user %}-e {{ spamassassin_milter_pref_default_domain }} -u {{ spamassassin_milter_pref_default_user }}{% endif %} -r {{ spamassassin_milter_reject_limit }} -C {{ spamassassin_milter_reject_code }} -R '{{ spamassassin_milter_reject_message }}' {% if spamassassin_milter_exclude_whitelisted_networks %}-i {{ spamassassin_milter_whitelisted_networks }}{% endif %} {% if spamassassin_milter_connect_to_external_spamd %}-- -d {{ spamassassin_milter_external_spamd_host }}:{{ spamassassin_milter_external_spamd_port }}{% endif %}" diff --git a/templates/spamassassin-db.cf.j2 b/templates/spamassassin-db.cf.j2 new file mode 100644 index 0000000..8df3e7f --- /dev/null +++ b/templates/spamassassin-db.cf.j2 @@ -0,0 +1,19 @@ +user_scores_dsn DBI:Pg:dbname={{ spamassassin_db_name }};host={{ spamassassin_db_host }};port={{ spamassassin_db_port }} +user_scores_sql_username {{ spamassassin_db_user }} +user_scores_sql_password {{ spamassassin_db_pwd }} +{% if spamassassin_use_bayes and spamassassin_bayes_sql_db %} +bayes_store_module Mail::SpamAssassin::BayesStore::PgSQL +bayes_sql_dsn DBI:Pg:dbname={{ spamassassin_db_name }};host={{ spamassassin_db_host }};port={{ spamassassin_db_port }} +bayes_sql_username {{ spamassassin_db_user }} +bayes_sql_password {{ spamassassin_db_pwd }} +{% endif %} +{% if spamassassin_auto_whitelist and spamassassin_auto_whitelist_sql_db %} +# awl should be deprecated in favor of txrep, but the perl plugin is missing +#loadplugin Mail::SpamAssassin::Plugin::TxRep +#txrep_factory Mail::SpamAssassin::SQLBasedAddrList +loadplugin Mail::SpamAssassin::Plugin::AWL +auto_whitelist_factory Mail::SpamAssassin::SQLBasedAddrList +user_awl_dsn DBI:Pg:dbname={{ spamassassin_db_name }};host={{ spamassassin_db_host }};port={{ spamassassin_db_port }} +user_awl_sql_username {{ spamassassin_db_user }} +user_awl_sql_password {{ spamassassin_db_pwd }} +{% endif %} diff --git a/templates/spamassassin-local.cf.j2 b/templates/spamassassin-local.cf.j2 new file mode 100644 index 0000000..a734ea9 --- /dev/null +++ b/templates/spamassassin-local.cf.j2 @@ -0,0 +1,19 @@ +# These values can be overridden by editing ~/.spamassassin/user_prefs.cf +# (see spamassassin(1) for details) + +# These should be safe assumptions and allow for simple visual sifting +# without risking lost emails. + +required_hits {{ spamassassin_required_hits }} +report_safe {{ spamassassin_report_safe }} +rewrite_header Subject {{ spamassassin_rewrite_subject }} +{% if spamassassin_use_bayes %} +use_bayes 1 +bayes_auto_learn {{ spamassassin_use_bayes_autolearn }} +bayes_auto_expire {{ spamassassin_use_bayes_auto_expire }} +{% else %} +use_bayes 0 +{% endif %} +{% if spamassassin_auto_whitelist %} +use_auto_whitelist 1 +{% endif %} diff --git a/templates/spamassassin_sysconfig.j2 b/templates/spamassassin_sysconfig.j2 new file mode 100644 index 0000000..3d2c8c1 --- /dev/null +++ b/templates/spamassassin_sysconfig.j2 @@ -0,0 +1,2 @@ +# Options to spamd +SPAMDOPTIONS="-d -m5 -H {% if spamassassin_db_user_config %}{{ spamassassin_spamd_sql_opts }}{% else %} -c{% endif %} {% if spamassassin_spamd_ssl_enabled %}{{ spamassassin_spamd_ssl_opts }}{% endif %}"