commit inicial
This commit is contained in:
commit
16c2f29f06
209
.gitignore
vendored
Normal file
209
.gitignore
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
# ---> Emacs
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# ---> Ansible
|
||||
*.retry
|
||||
|
||||
# ---> Linux
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
# ---> macOS
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Postfix Ansible role
|
||||
|
||||
Rol de Ansible para configurar el servidor SMTP Postfix.
|
369
action_plugins/postconf.py
Executable file
369
action_plugins/postconf.py
Executable file
@ -0,0 +1,369 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2018, Mauro Torrez <maurotorrez@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.vars import merge_hash
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
class PfWorkflowError(Exception):
|
||||
pass
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
def getarg(self, key, default=True):
|
||||
if isinstance(default, basestring):
|
||||
if key == 'parameter':
|
||||
return self._task.args.get(key, default)
|
||||
return '{}'.format(self._task.args.get(key, default))
|
||||
if default:
|
||||
if key == 'state':
|
||||
return '{}'.format(self._task.args.get(key, 'present'))
|
||||
if key == 'parameter':
|
||||
return self._task.args.get(key, '')
|
||||
if key in ('service', 'command', 'value', 'type'):
|
||||
return '{}'.format(self._task.args.get(key, ''))
|
||||
if key in ('private', 'unprivileged', 'chroot', 'wakeup', 'process_limit'):
|
||||
return '{}'.format(self._task.args.get(key, ''))
|
||||
# no default value for key: return as-is
|
||||
return self._task.args.get(key)
|
||||
|
||||
|
||||
def runcmd(self, reg_name, cmd, param=None):
|
||||
try:
|
||||
if param:
|
||||
self.reg[param][reg_name] = self._execute_module(
|
||||
module_name='command',
|
||||
module_args=dict(_raw_params=cmd)
|
||||
)
|
||||
return
|
||||
self.reg[reg_name] = self._execute_module(
|
||||
module_name='command',
|
||||
module_args=dict(_raw_params=cmd)
|
||||
)
|
||||
except Exception as e:
|
||||
raise PfWorkflowError(
|
||||
"{}: {}; failed command line: {}".format(
|
||||
type(e).__name__,
|
||||
to_text(e),
|
||||
cmd
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validate_arguments(self):
|
||||
'''assert arguments are valid'''
|
||||
|
||||
# validate service name
|
||||
self.state = self.getarg('state')
|
||||
if self.state not in ('present', 'absent', 'prepend', 'append'):
|
||||
raise PfWorkflowError(
|
||||
'Invalid state requested: use "present", "absent", "prepend" or "append"'
|
||||
)
|
||||
|
||||
# validate service name
|
||||
self.service = self.getarg('service')
|
||||
if re.search('[^a-zA-Z0-9_]',self.service):
|
||||
raise PfWorkflowError(
|
||||
'Invalid character found in service identifier'
|
||||
)
|
||||
|
||||
# validate service type text
|
||||
self.stype = self.getarg('type')
|
||||
if re.search('[^a-zA-Z0-9_]',self.stype):
|
||||
raise PfWorkflowError(
|
||||
'Invalid character found in service type identifier'
|
||||
)
|
||||
|
||||
# validate flags
|
||||
for p in ('private', 'unprivileged', 'chroot'):
|
||||
if re.search('[^yn-]',self.getarg(p)):
|
||||
raise PfWorkflowError(
|
||||
'Invalid value for boolean flag {}'.format(p)
|
||||
)
|
||||
|
||||
# validate flags
|
||||
if re.search('[^0-9?-]',self.getarg('wakeup')):
|
||||
raise PfWorkflowError(
|
||||
'Invalid value for wakeup flag: {}'.format(self.getarg('wakeup'))
|
||||
)
|
||||
|
||||
# validate flags
|
||||
if re.search('[^0-9-]',self.getarg('process_limit')):
|
||||
raise PfWorkflowError(
|
||||
'Invalid value for process_limit flag: {}'.format(self.getarg('process_limit'))
|
||||
)
|
||||
|
||||
|
||||
def set_service(self):
|
||||
'''
|
||||
Set service entry in master.cf
|
||||
'''
|
||||
if not self.service:
|
||||
self.reg['service_skipped'] = True
|
||||
return
|
||||
|
||||
state = self.getarg('state')
|
||||
|
||||
# guess existing entry ------------------------------------------------
|
||||
|
||||
# get current service entry (if exists)
|
||||
cmdline = 'postconf -M {}'.format(self.service)
|
||||
if self.stype:
|
||||
# append service type to the command line
|
||||
cmdline = '{}/{}'.format(cmdline,self.stype)
|
||||
|
||||
self.runcmd('cmd_initial_check',cmdline)
|
||||
|
||||
# check service is defined and unambiguous by counting stdout lines
|
||||
if len(self.reg['cmd_initial_check']['stdout_lines']) > 1:
|
||||
raise PfWorkflowError(
|
||||
'Ambiguous service identifier, please provide service type'
|
||||
)
|
||||
|
||||
elif len(self.reg['cmd_initial_check']['stdout_lines']) == 1:
|
||||
if not self.stype:
|
||||
self.stype = self.reg['cmd_initial_check']['stdout'].split()[1]
|
||||
|
||||
# fill service fields as given by postconf
|
||||
cmdline = 'postconf -Fh \
|
||||
{0}/{1}/private {0}/{1}/unprivileged {0}/{1}/chroot \
|
||||
{0}/{1}/wakeup {0}/{1}/process_limit {0}/{1}/command'.format(
|
||||
self.service, self.stype)
|
||||
self.runcmd('cmd_fields_check',cmdline)
|
||||
|
||||
private = self.getarg(
|
||||
'private',
|
||||
self.reg['cmd_fields_check']['stdout_lines'][0]
|
||||
)
|
||||
unprivileged = self.getarg(
|
||||
'unprivileged',
|
||||
self.reg['cmd_fields_check']['stdout_lines'][1]
|
||||
)
|
||||
chroot = self.getarg(
|
||||
'chroot',
|
||||
self.reg['cmd_fields_check']['stdout_lines'][2]
|
||||
)
|
||||
wakeup = self.getarg(
|
||||
'wakeup',
|
||||
self.reg['cmd_fields_check']['stdout_lines'][3]
|
||||
)
|
||||
process_limit = self.getarg(
|
||||
'process_limit',
|
||||
self.reg['cmd_fields_check']['stdout_lines'][4]
|
||||
)
|
||||
|
||||
# change the command field when set explicitly
|
||||
command = self.reg['cmd_fields_check']['stdout_lines'][5]
|
||||
if self.getarg('command') and command.split()[0] != self.getarg('command'):
|
||||
command = self.getarg('command')
|
||||
|
||||
else:
|
||||
# no entry for service, create it if state != absent
|
||||
if state != 'absent':
|
||||
private = self.getarg('private','-')
|
||||
unprivileged = self.getarg('unprivileged','-')
|
||||
chroot = self.getarg('chroot','-')
|
||||
wakeup = self.getarg('wakeup','-')
|
||||
process_limit = self.getarg('process_limit','-')
|
||||
command = self.getarg('command')
|
||||
|
||||
cmdline = 'postconf -M {0}/{1}="{0} {1} {2} {3} {4} {5} {6} {7}"'.format(
|
||||
self.service,
|
||||
self.stype,
|
||||
private,
|
||||
unprivileged,
|
||||
chroot,
|
||||
wakeup,
|
||||
process_limit,
|
||||
command
|
||||
)
|
||||
self.runcmd('cmd_add_service_entry',cmdline)
|
||||
|
||||
|
||||
# state = absent ------------------------------------------------------
|
||||
if state == 'absent':
|
||||
if self.getarg('parameter'):
|
||||
# state=absent with parameter does not remove service entry
|
||||
pass
|
||||
else:
|
||||
# remove whole service definition
|
||||
cmdline = 'postconf -M# {}/{}'.format(service,stype)
|
||||
self.runcmd('cmd_remove_service',cmdline)
|
||||
|
||||
# non-absent states: set all fields -----------------------------------
|
||||
else:
|
||||
cmdline = 'postconf -F \
|
||||
{0}/{1}/private={2} {0}/{1}/unprivileged={3} {0}/{1}/chroot={4} \
|
||||
{0}/{1}/wakeup={5} {0}/{1}/process_limit={6} {0}/{1}/command={7}'.format(
|
||||
self.service, self.stype, private, unprivileged, chroot, wakeup, process_limit,
|
||||
'"' + command.replace(r'\\',r'\\\\').replace(r'"',r'\"') + '"'
|
||||
)
|
||||
self.runcmd('cmd_fields_set',cmdline)
|
||||
|
||||
|
||||
# verify changes
|
||||
cmdline = 'postconf -M {}/{}'.format(self.service,self.stype)
|
||||
self.runcmd('cmd_final_check',cmdline)
|
||||
|
||||
self.reg['service_changed'] = True
|
||||
if self.reg['cmd_initial_check']['stdout'] == self.reg['cmd_final_check']['stdout']:
|
||||
self.reg['service_changed'] = False
|
||||
|
||||
|
||||
def set_single_parameter(self, parameter='', value='', state=''):
|
||||
'''
|
||||
Set a single parameter value in main.cf (service=None) or master.cf
|
||||
'''
|
||||
|
||||
# value should be string
|
||||
if not isinstance(value, basestring):
|
||||
try:
|
||||
value = ', '.join(value)
|
||||
except TypeError:
|
||||
value = str(value)
|
||||
|
||||
# default command to execute for setting the parameter
|
||||
postconf_cmd = 'postconf'
|
||||
|
||||
# validate parameter identifier
|
||||
if not parameter or re.search('[^a-zA-Z0-9_-]', parameter):
|
||||
raise PfWorkflowError(
|
||||
'Invalid parameter identifier: «{}»'.format(parameter)
|
||||
)
|
||||
|
||||
regkey = "param_{}".format(parameter)
|
||||
self.reg[regkey] = {}
|
||||
|
||||
if self.service:
|
||||
# change command to execute for setting the parameter
|
||||
postconf_cmd = 'postconf -P'
|
||||
# change parameter identifier to include service/type
|
||||
parameter = '/'.join([self.service,self.stype,parameter])
|
||||
|
||||
#
|
||||
# general logic for setting parameter
|
||||
#
|
||||
|
||||
# 1: get current value
|
||||
cmdline = "{} -h {}".format(postconf_cmd, parameter)
|
||||
self.runcmd('cmd_initial_check',cmdline,regkey)
|
||||
current_value = self.reg[regkey]['cmd_initial_check']['stdout']
|
||||
|
||||
# 2: act according to desired state
|
||||
if state == 'absent':
|
||||
cmdline = "{} -X {}".format(postconf_cmd, parameter)
|
||||
self.runcmd('cmd_remove',cmdline,regkey)
|
||||
|
||||
elif state in ('append', 'prepend'):
|
||||
# we assume the current value to be a comma-separated list,
|
||||
# and check if desired value is contained in current value
|
||||
|
||||
if value not in current_value:
|
||||
|
||||
# only modify parameter if desired value not contained in current
|
||||
if state == 'prepend':
|
||||
new_value = ', '.join([value,current_value])
|
||||
else:
|
||||
new_value = ', '.join([current_value,value])
|
||||
|
||||
# escape value according to shlex posix mode
|
||||
escaped_value= '"' + new_value.replace(
|
||||
r'\\',r'\\\\').replace(r'"',r'\"')+'"'
|
||||
cmdline = "{} -e {}={}".format(postconf_cmd, parameter, escaped_value)
|
||||
self.runcmd('cmd_modify',cmdline,regkey)
|
||||
|
||||
elif state == 'present':
|
||||
if value.strip() != current_value.strip():
|
||||
# set value exactly as required
|
||||
# escape value according to shlex posix mode
|
||||
escaped_value= '"' + value.replace(
|
||||
r'\\',r'\\\\').replace(r'"',r'\"')+'"'
|
||||
cmdline = "{} -e {}={}".format(postconf_cmd, parameter, escaped_value)
|
||||
self.runcmd('cmd_modify_exact',cmdline,regkey)
|
||||
|
||||
else:
|
||||
# unsupported state
|
||||
raise PfWorkflowError(
|
||||
"Unsupported value in «state»"
|
||||
)
|
||||
|
||||
# finally check how the value has changed
|
||||
cmdline = "{} -h {}".format(postconf_cmd, parameter)
|
||||
self.runcmd('cmd_final_check',cmdline,regkey)
|
||||
|
||||
changed = self.reg.get('parameter_changed', False)
|
||||
final_value = self.reg[regkey]['cmd_final_check']['stdout']
|
||||
if final_value != current_value:
|
||||
changed = True
|
||||
|
||||
self.reg['parameter_changed'] = changed
|
||||
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
if result.get('skipped'):
|
||||
return result
|
||||
|
||||
try:
|
||||
self.validate_arguments()
|
||||
except PfWorkflowError as e:
|
||||
return {
|
||||
'failed': True,
|
||||
'msg': e.message
|
||||
}
|
||||
|
||||
self.reg = {}
|
||||
|
||||
# process service
|
||||
if self.getarg('service',False):
|
||||
self.set_service()
|
||||
|
||||
parameter = self.getarg('parameter')
|
||||
state = self.getarg('state')
|
||||
if parameter:
|
||||
# build (parameter, value) pairs for iteration
|
||||
if isinstance(parameter, basestring):
|
||||
pv = [ (parameter, self.getarg('value')) ]
|
||||
elif isinstance(parameter, dict):
|
||||
# if dict ignore new_value
|
||||
pv = parameter.items()
|
||||
else:
|
||||
# assume list --> only supported for 'absent'
|
||||
pv = zip(parameter, parameter)
|
||||
|
||||
for p in pv:
|
||||
# loop over parameters and exit if error
|
||||
self.set_single_parameter(p[0], p[1], state=state)
|
||||
else:
|
||||
self.reg['parameter_skipped'] = True
|
||||
|
||||
if all((self.reg.get('service_skipped',False),
|
||||
self.reg.get('parameter_skipped',False))):
|
||||
return {
|
||||
'failed': True,
|
||||
'msg': 'FATAL: no action requested!'
|
||||
}
|
||||
|
||||
result['changed'] = any((self.reg.get('service_changed'),
|
||||
self.reg.get('parameter_changed')))
|
||||
result['reg'] = self.reg
|
||||
return result
|
271
defaults/main.yml
Normal file
271
defaults/main.yml
Normal file
@ -0,0 +1,271 @@
|
||||
---
|
||||
|
||||
# Postfix config directory (changing this is probably a bad idea)
|
||||
postfix_config_dir: "/etc/postfix"
|
||||
|
||||
# Where to save access lists
|
||||
postfix_rules_dir: "{{ postfix_config_dir }}/rules"
|
||||
|
||||
# accepted email domains
|
||||
postfix_mail_domains: "{{ mail_domains | default(['example.com']) }}"
|
||||
|
||||
# postfix server domain: used for identification of the server
|
||||
postfix_server_domain: "{{ postfix_mail_domains | first }}"
|
||||
|
||||
# postfix server identification
|
||||
postfix_server_name: "mail.{{ postfix_server_domain }}"
|
||||
|
||||
# domains considered as "local" unix domains (local server users)
|
||||
# ansible_fqdn, localhost.localdomain, and localhost are always added regardless of this value
|
||||
postfix_unix_domains: []
|
||||
|
||||
# networks considered "local"
|
||||
# 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 are always added regardless of this value
|
||||
postfix_local_networks: []
|
||||
|
||||
# domain-specific configuration
|
||||
postfix_domain_config: {}
|
||||
|
||||
# LDAP ------------------------------------------------------------------------
|
||||
|
||||
# Default LDAP connection parameters
|
||||
postfix_ldap_server: "localhost"
|
||||
postfix_ldap_port: 389
|
||||
postfix_ldap_version: 3
|
||||
postfix_ldap_scope: "sub"
|
||||
postfix_ldap_bind: no
|
||||
postfix_ldap_bind_dn: ''
|
||||
postfix_ldap_bind_pw: ''
|
||||
postfix_ldap_start_tls: no
|
||||
postfix_ldap_tls_ca_cert_dir: ''
|
||||
postfix_ldap_tls_ca_cert_file: ''
|
||||
postfix_ldap_use_group_alias: yes
|
||||
|
||||
# SQLITE ----------------------------------------------------------------------
|
||||
|
||||
postfix_sqlite_user_query: "SELECT name FROM users WHERE name = '%u'"
|
||||
postfix_sqlite_alias_query: "SELECT dest FROM aliases WHERE alias = '%s'"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# enable submission service?
|
||||
postfix_submission_enable: yes
|
||||
|
||||
# TLS -------------------------------------------------------------------------
|
||||
|
||||
# TLS certificate/private key to use
|
||||
postfix_tls_certificate: "{{ tls_certificate | default('') }}"
|
||||
postfix_tls_private_key: "{{ tls_certificate_key | default('') }}"
|
||||
|
||||
# level of encryption to use for sending mail to the Internet
|
||||
# only change this if you know what the implications are, see
|
||||
# http://www.postfix.org/postconf.5.html#smtp_tls_security_level
|
||||
# possible values: none|may|encrypt|dane|dane-only|fingerprint|verify|secure
|
||||
postfix_outgoing_tls_security: 'may'
|
||||
|
||||
# level of encryption required for receiving mail from the Internet
|
||||
# only change this if you know what the implications are, see
|
||||
# http://www.postfix.org/postconf.5.html#smtpd_tls_security_level
|
||||
postfix_incoming_tls_security: 'may'
|
||||
|
||||
# wether to allow insecure (plaintext) login from clients
|
||||
postfix_allow_insecure_auth: no
|
||||
|
||||
# enable/disable tls session cache
|
||||
postfix_tls_session_cache: yes
|
||||
|
||||
# SASL ------------------------------------------------------------------------
|
||||
|
||||
# wether to allow SASL authentication on smtpd (MX port 25)
|
||||
postfix_enable_smtpd_auth: no
|
||||
|
||||
# creo que esto no sirve para nada
|
||||
# smtpd_sasl_local_domain: $myhostname
|
||||
|
||||
# valor por defecto ya es noanonymous
|
||||
# smtpd_sasl_security_options: noanonymous
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# MESSAGE SIZE LIMIT ----------------------------------------------------------
|
||||
|
||||
# This value should be fine for most people. Note that increasing this limit
|
||||
# does not guarantee delivery of very big emails: remote server size
|
||||
# restrictions still apply.
|
||||
postfix_message_size_limit: 31457280
|
||||
|
||||
# HELO required ---------------------------------------------------------------
|
||||
postfix_helo_required: yes
|
||||
|
||||
# biff enabled
|
||||
postfix_biff: no
|
||||
|
||||
# postfix_bounce_queue_lifetime: 2d
|
||||
# postfix_maximal_queue_lifetime: 2d
|
||||
# postfix_compatibility_level = 2 DEFAULT 0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# RESTRICTIONS AND ACCESS LISTS -----------------------------------------------
|
||||
|
||||
# Host-based access list: PCRE table, for wildcard support
|
||||
# every item should contain a 'regex' field and a corresponding 'action' field
|
||||
postfix_client_access_list: []
|
||||
# - regex: '172\.16\.192\.0\/24'
|
||||
# action: 'REJECT Please use alternative server'
|
||||
# - regex: 'rude\.client\.com'
|
||||
# action: 'REJECT Get out!'
|
||||
|
||||
# Restricciones aplicadas a los clientes SMTP
|
||||
postfix_client_restrictions:
|
||||
- check_client_access pcre:{{ postfix_rules_dir }}/client_access_list
|
||||
- permit_sasl_authenticated
|
||||
- permit_mynetworks
|
||||
- reject_unknown_client_hostname
|
||||
- reject_unauth_pipelining
|
||||
- permit
|
||||
|
||||
# Restricciones aplicadas en SMTP DATA
|
||||
postfix_data_restrictions:
|
||||
- reject_unauth_pipelining
|
||||
- permit
|
||||
|
||||
# HELO access list: reject/accept clients by their HELO hostname
|
||||
# Hash table, list of items with "host" and corresponding "action" fields
|
||||
postfix_helo_access_list: []
|
||||
# - host: "{{ansible_ip}}"
|
||||
# action: "REJECT You can't be me. Get out!"
|
||||
# - host: "{{ansible_fqdn}}"
|
||||
# action: "REJECT You can't be me. Get out!"
|
||||
|
||||
# Requerir HELO/EHLO y aplicarle restricciones
|
||||
postfix_helo_restrictions:
|
||||
- check_helo_access hash:{{ postfix_rules_dir }}/helo_access_list
|
||||
- permit_mynetworks
|
||||
- reject_non_fqdn_helo_hostname
|
||||
- reject_invalid_helo_hostname
|
||||
- warn_if_reject
|
||||
- permit
|
||||
|
||||
# Restricciones de control de relay aplicadas en RCPT TO, antes de smtpd_recipient_restrictions
|
||||
postfix_relay_restrictions:
|
||||
- permit_mynetworks
|
||||
- permit_sasl_authenticated
|
||||
- reject_unauth_destination
|
||||
- permit
|
||||
|
||||
# Recipient access list: reject/accept mail by RCPT TO recipients
|
||||
postfix_recipient_access_list: []
|
||||
# - rcpt: "emailvalidation.helpdesk01@gmail.com"
|
||||
# action: "REJECT Forbidden recipient. Get out!"
|
||||
|
||||
# Restricciones al destinatario especificado en RCPT TO
|
||||
postfix_recipient_restrictions:
|
||||
- check_recipient_access hash:{{ postfix_rules_dir }}/recipient_access_list
|
||||
- permit_mynetworks
|
||||
- permit_sasl_authenticated
|
||||
- reject_unknown_recipient_domain
|
||||
- reject_non_fqdn_recipient
|
||||
- reject_unauth_destination
|
||||
- permit
|
||||
|
||||
# Recipient access list: reject/accept mail by MAIL FROM sender
|
||||
postfix_sender_access_list: []
|
||||
# - sender: "@addr.com"
|
||||
# action: "REJECT We're fed up with your spam. Get out!"
|
||||
|
||||
# Restricciones aplicadas al remitente especificado en MAIL FROM
|
||||
postfix_sender_restrictions:
|
||||
- check_sender_access hash:{{ postfix_rules_dir }}/sender_access_list
|
||||
- permit_sasl_authenticated
|
||||
- permit_mynetworks
|
||||
- reject_unknown_sender_domain
|
||||
- reject_non_fqdn_sender
|
||||
- permit
|
||||
|
||||
# configuracion postscreen ----------------------------------------------------
|
||||
# ver http://www.postfix.org/POSTSCREEN_README.html
|
||||
# y tambien http://www.postfix.org/postscreen.8.html
|
||||
|
||||
# habilitar postscreen?
|
||||
postfix_postscreen_enable: yes
|
||||
|
||||
# lista blanca/negra de IPs (solo se permiten valores ip, ip/netmask)
|
||||
postfix_postscreen_access_list: []
|
||||
# action = (permit|dunno|reject). Ejemplos:
|
||||
# - address: "127.0.0.0/8"
|
||||
# action: dunno
|
||||
# - address: "2001:db8::/32"
|
||||
# action: reject
|
||||
|
||||
# lista de sitios y ponderacion a usar como criterio dnsbl
|
||||
# cada item puede ser un string o un dict con atributos:
|
||||
# .item, .score (opcional, default=1) y .mask (opcional, para
|
||||
# ocultar la lista a clientes remotos)
|
||||
postfix_postscreen_dnsbl_sites:
|
||||
- site: zen.spamhaus.org
|
||||
score: 3
|
||||
- site: b.barracudacentral.org
|
||||
score: 2
|
||||
- site: bl.spameatingmonkey.net
|
||||
score: 2
|
||||
mask: spameatingmonkey.com
|
||||
- site: bl.spamcop.net
|
||||
- dnsbl.sorbs.net
|
||||
- site: psbl.surriel.com
|
||||
- bl.mailspike.net
|
||||
- site: swl.spamhaus.org
|
||||
score: -4 # whitelist
|
||||
# example:
|
||||
# - site: mypassword.bl.service.com
|
||||
# score: 3
|
||||
# mask: service.com
|
||||
|
||||
# acción a efectuar cuando el cliente está en la lista negra (access_list)
|
||||
postfix_postscreen_blacklist_action: drop
|
||||
|
||||
# habilitar tests bare_newline? (no recomendado)
|
||||
postfix_postscreen_bare_newline_enable: no
|
||||
|
||||
# acción a efectuar cuando el cliente no cumple el test bare_newline
|
||||
postfix_postscreen_bare_newline_action: ignore
|
||||
|
||||
# acción a efectuar cuando el cliente está en una lista dnsbl (enforce|ignore|drop)
|
||||
postfix_postscreen_dnsbl_action: enforce
|
||||
|
||||
# mapeo que determina cuáles dnsbls informar al cliente como razón del rechazo a la conexion
|
||||
postfix_postscreen_dnsbl_reply_map: "pcre:$config_directory/reglas/postscreen_dnsbl_reply_map.pcre"
|
||||
|
||||
# umbral a superar para considerar al host remoto como spammer
|
||||
postfix_postscreen_dnsbl_threshold: 3
|
||||
|
||||
# cuando esta bajo de este puntaje, no se hacen mas tests y se pasa a una whitelist
|
||||
postfix_postscreen_dnsbl_whitelist_threshold: -1
|
||||
|
||||
# accion a efectuar cuando el cliente habla antes de su turno (enforce|ignore|drop)
|
||||
postfix_postscreen_greet_action: enforce
|
||||
|
||||
# tiempo a esperar para detectar un cliente malo
|
||||
postfix_postscreen_greet_wait: "${stress?{2}:{6}}s"
|
||||
|
||||
# habilitar deteccion de comandos no-smtp? (no recomendado)
|
||||
postfix_postscreen_non_smtp_command_enable: no
|
||||
|
||||
# accion a efectuar cuando no se cumple el test non_smtp_command
|
||||
postfix_postscreen_non_smtp_command_action: drop
|
||||
|
||||
# habilitar tests pipelining? (no recomendado)
|
||||
postfix_postscreen_pipelining_enable: no
|
||||
|
||||
# accion a efectuar cuando no se cumple pipelining (enforce|ignore|drop)
|
||||
postfix_postscreen_pipelining_action: enforce
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Hosts considerados como test, obligados a usar otro servidor
|
||||
# El valor debe ser una regexp válida
|
||||
# correo_hosts_test: []
|
||||
|
||||
# redes internas obligadas a utilizar servicio submission
|
||||
# correo_forzar_submission: []
|
15
filter_plugins/belist.py
Normal file
15
filter_plugins/belist.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import Sequence
|
||||
def assert_list(arg):
|
||||
''' Convert string argument to list-of-strings '''
|
||||
if isinstance(arg, basestring):
|
||||
return [ arg ]
|
||||
if not isinstance(arg, Sequence):
|
||||
return [ arg ]
|
||||
return arg
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
'belist': assert_list
|
||||
}
|
15
filter_plugins/binary.py
Normal file
15
filter_plugins/binary.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import Sequence
|
||||
def binary(arg,yes_value='yes',no_value='no'):
|
||||
''' Convert boolean argument to 'yes' or 'no' string value '''
|
||||
if isinstance(arg, bool):
|
||||
if arg:
|
||||
return yes_value
|
||||
return no_value
|
||||
return arg
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
'binary': binary
|
||||
}
|
32
handlers/main.yml
Normal file
32
handlers/main.yml
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: restart postfix
|
||||
service: name=postfix state=restarted
|
||||
|
||||
- name: reload postfix
|
||||
service: name=postfix state=restarted
|
||||
|
||||
- name: newaliases
|
||||
command: newaliases
|
||||
|
||||
- name: postmap hash aliases
|
||||
command: "postmap hash:{{ dc[item]['alias_lookup']['file'] }}"
|
||||
when:
|
||||
- "dc[item]['alias_lookup']['provider'] == 'file'"
|
||||
with_items: "{{ postfix_mail_domains|belist }}"
|
||||
|
||||
- name: postmap hash users
|
||||
command: "postmap hash:{{ dc[item]['user_lookup']['file'] }}"
|
||||
when:
|
||||
- "dc[item]['user_lookup']['provider'] == 'file'"
|
||||
with_items: "{{ postfix_mail_domains|belist }}"
|
||||
|
||||
- name: postmap no reply aliases
|
||||
command: "postmap hash:{{ dc[item]['noreply_file'] }}"
|
||||
with_items: "{{ postfix_mail_domains|belist }}"
|
||||
|
||||
- name: postmap access lists
|
||||
command: postmap {{item}}
|
||||
with_items:
|
||||
- "{{ postfix_rules_dir }}/helo_access_list"
|
||||
- "{{ postfix_rules_dir }}/recipient_access_list"
|
||||
- "{{ postfix_rules_dir }}/sender_access_list"
|
255
tasks/main.yml
Normal file
255
tasks/main.yml
Normal file
@ -0,0 +1,255 @@
|
||||
---
|
||||
- name: "Load default config for domains"
|
||||
set_fact:
|
||||
dc: "{{ dc|default({})|combine( { item: {
|
||||
'user_lookup': {
|
||||
'provider': 'file',
|
||||
'file': vmail_home +'/'+item+'_users',
|
||||
'domain': item,
|
||||
'server_host': postfix_ldap_server,
|
||||
'server_port': postfix_ldap_port,
|
||||
'version': postfix_ldap_version,
|
||||
'scope': postfix_ldap_scope,
|
||||
'bind': postfix_ldap_bind,
|
||||
'bind_dn': postfix_ldap_bind_dn,
|
||||
'bind_pw': postfix_ldap_bind_pw,
|
||||
'start_tls': postfix_ldap_start_tls,
|
||||
'tls_ca_cert_file': postfix_ldap_tls_ca_cert_file,
|
||||
'tls_ca_cert_dir': postfix_ldap_tls_ca_cert_dir,
|
||||
'search_base':
|
||||
'ou=People,'+item.split('.')|map('regex_replace','^','dc=')|join(','),
|
||||
'query_filter': '(&(objectClass=inetOrgPerson)(uid=%u))',
|
||||
'result_attribute': 'uid',
|
||||
'result_format': vmail_home+'/mail/'+item+'/%s/',
|
||||
'dbpath': vmail_home+'/'+item+'_users.sqlite',
|
||||
'query': postfix_sqlite_user_query
|
||||
},
|
||||
'users': [],
|
||||
'alias_lookup': {
|
||||
'provider': 'file',
|
||||
'file': vmail_home +'/'+item+'_aliases',
|
||||
'domain': item,
|
||||
'server_host': postfix_ldap_server,
|
||||
'server_port': postfix_ldap_port,
|
||||
'version': postfix_ldap_version,
|
||||
'scope': postfix_ldap_scope,
|
||||
'bind': postfix_ldap_bind,
|
||||
'bind_dn': postfix_ldap_bind_dn,
|
||||
'bind_pw': postfix_ldap_bind_pw,
|
||||
'start_tls': postfix_ldap_start_tls,
|
||||
'tls_ca_cert_file': postfix_ldap_tls_ca_cert_file,
|
||||
'tls_ca_cert_dir': postfix_ldap_tls_ca_cert_dir,
|
||||
'search_base':
|
||||
'ou=Alias,'+item.split('.')|map('regex_replace','^','dc=')|join(','),
|
||||
'query_filter': '(&(objectClass=nisMailAlias)(cn=%u))',
|
||||
'result_attribute': 'rfc822MailMember',
|
||||
'result_format': '%s',
|
||||
'dbpath': vmail_home+'/'+item+'_aliases.sqlite',
|
||||
'query': postfix_sqlite_alias_query
|
||||
},
|
||||
'aliases': [],
|
||||
'use_group_as_alias': postfix_ldap_use_group_alias,
|
||||
'group_lookup': {
|
||||
'provider': 'ldap',
|
||||
'domain': item,
|
||||
'server_host': postfix_ldap_server,
|
||||
'server_port': postfix_ldap_port,
|
||||
'version': postfix_ldap_version,
|
||||
'scope': postfix_ldap_scope,
|
||||
'bind': postfix_ldap_bind,
|
||||
'bind_dn': postfix_ldap_bind_dn,
|
||||
'bind_pw': postfix_ldap_bind_pw,
|
||||
'start_tls': postfix_ldap_start_tls,
|
||||
'tls_ca_cert_file': postfix_ldap_tls_ca_cert_file,
|
||||
'tls_ca_cert_dir': postfix_ldap_tls_ca_cert_dir,
|
||||
'search_base':
|
||||
'ou=Group,'+item.split('.')|map('regex_replace','^','dc=')|join(','),
|
||||
'query_filter': '(&(objectClass=posixGroup)(cn=%u))',
|
||||
'result_attribute': 'memberUid',
|
||||
'result_format': '%s@{{d}}',
|
||||
},
|
||||
'noreply_aliases': [ 'noreply' ],
|
||||
'noreply_file': vmail_home +'/'+item+'_noreply',
|
||||
} }, recursive=True) }}"
|
||||
with_items: "{{ postfix_mail_domains|belist }}"
|
||||
|
||||
- name: "Override config for domains"
|
||||
set_fact:
|
||||
dc: '{{ dc | combine(postfix_domain_config, recursive=True) }}'
|
||||
|
||||
- apt: name=postfix update_cache=yes
|
||||
- apt: name=postfix-pcre
|
||||
notify: restart postfix
|
||||
|
||||
- apt: name=postfix-ldap
|
||||
when:
|
||||
# see http://jmespath.org/
|
||||
- '"ldap" in dc|json_query("*.[ alias_lookup, user_lookup ][].provider")'
|
||||
notify: restart postfix
|
||||
- apt: name=postfix-sqlite
|
||||
when:
|
||||
# see http://jmespath.org/
|
||||
- '"sqlite" in dc|json_query("*.[ alias_lookup, user_lookup ][].provider")'
|
||||
notify: restart postfix
|
||||
|
||||
- name: "Template Dovecot delivery/auth service config for Postfix"
|
||||
template:
|
||||
src: 11-postfix.conf.j2
|
||||
dest: /etc/dovecot/conf.d/11-postfix.conf
|
||||
notify: restart dovecot
|
||||
|
||||
- name: "Configure lookup tables"
|
||||
include_tasks: lookup_tables.yml
|
||||
with_items: "{{postfix_mail_domains|belist}}"
|
||||
loop_control:
|
||||
loop_var: "domain"
|
||||
|
||||
- name: "Configure no-reply local mail alias"
|
||||
blockinfile:
|
||||
block: |
|
||||
_dev_null: /dev/null
|
||||
marker: "# {mark} ANSIBLE-MANAGED ALIASES"
|
||||
path: "/etc/aliases"
|
||||
notify: newaliases
|
||||
|
||||
- name: "Create rules directory for access lists"
|
||||
file:
|
||||
name: "{{ postfix_rules_dir }}"
|
||||
state: directory
|
||||
|
||||
- name: "Template client access list"
|
||||
blockinfile:
|
||||
path: "{{ postfix_rules_dir }}/client_access_list"
|
||||
create: yes
|
||||
block: |
|
||||
# Edit host variable `postfix_client_access_list` to change these values
|
||||
{% for entry in postfix_client_access_list -%}
|
||||
{{ entry.regex }} {{ entry.action }}
|
||||
{% endfor %}
|
||||
|
||||
- name: "Template helo access list"
|
||||
blockinfile:
|
||||
path: "{{ postfix_rules_dir }}/helo_access_list"
|
||||
create: yes
|
||||
block: |
|
||||
# Edit host variable `postfix_helo_access_list` to change these values
|
||||
{% for entry in postfix_helo_access_list -%}
|
||||
{{ entry.host }} {{ entry.action }}
|
||||
{% endfor %}
|
||||
notify: postmap access lists
|
||||
|
||||
- name: "Template recipient access list"
|
||||
blockinfile:
|
||||
path: "{{ postfix_rules_dir }}/recipient_access_list"
|
||||
create: yes
|
||||
block: |
|
||||
# Edit host variable `postfix_recipient_access_list` to change these values
|
||||
{% for entry in postfix_recipient_access_list -%}
|
||||
{{ entry.rcpt }} {{ entry.action }}
|
||||
{% endfor %}
|
||||
notify: postmap access lists
|
||||
|
||||
- name: "Template sender access list"
|
||||
blockinfile:
|
||||
path: "{{ postfix_rules_dir }}/sender_access_list"
|
||||
create: yes
|
||||
block: |
|
||||
# Edit host variable `postfix_sender_access_list` to change these values
|
||||
{% for entry in postfix_sender_access_list -%}
|
||||
{{ entry.sender }} {{ entry.action }}
|
||||
{% endfor %}
|
||||
notify: postmap access lists
|
||||
|
||||
- name: "Set main.cf parameters"
|
||||
postconf:
|
||||
parameter:
|
||||
mydestination: >-
|
||||
{{ postfix_unix_domains | belist |
|
||||
union( [ ansible_fqdn, 'localhost.localdomain', 'localhost'] ) |
|
||||
difference( postfix_mail_domains|belist ) }}
|
||||
myhostname: "{{ postfix_server_name }}"
|
||||
mydomain: "{{ postfix_server_domain }}"
|
||||
mynetworks: >-
|
||||
{{ ['127.0.0.0/8', '[::ffff:127.0.0.0]/104', '[::1]/128'] |
|
||||
union( postfix_local_networks ) }}
|
||||
virtual_alias_maps: >-
|
||||
{% for d in postfix_mail_domains|belist %}
|
||||
{% set p = dc[d]['alias_lookup']['provider'] %}
|
||||
{% if p == "ldap" %}
|
||||
ldap:{{ postfix_config_dir }}/{{d}}_ldap_alias.cf
|
||||
{% if dc[d]['use_group_as_alias'] %},
|
||||
ldap:{{ postfix_config_dir }}/{{d}}_ldap_group.cf
|
||||
{% endif %}
|
||||
{% elif p == "sqlite" %}
|
||||
sqlite:{{ postfix_config_dir }}/{{d}}_sqlite_alias.cf
|
||||
{% elif p == "file" %}
|
||||
hash:{{ vmail_home }}/{{d}}_aliases
|
||||
{% endif %}{{ '' if loop.last else ',' }}{% endfor %},
|
||||
hash:{{ postfix_config_dir }}/noreply_aliases
|
||||
virtual_mailbox_maps: >-
|
||||
{% for d in postfix_mail_domains|belist %}
|
||||
{% set p = dc[d]['user_lookup']['provider'] %}
|
||||
{% if p == "ldap" %}
|
||||
ldap:{{ postfix_config_dir }}/{{d}}_ldap_user.cf
|
||||
{% elif p == "sqlite" %}
|
||||
sqlite:{{ postfix_config_dir }}/{{d}}_sqlite_user.cf
|
||||
{% elif p == "file" %}
|
||||
hash:{{ vmail_home }}/{{d}}_users
|
||||
{% endif %}{{ '' if loop.last else ',' }}{% endfor %},
|
||||
virtual_transport: "lmtp:unix:private/dovecot-lmtp"
|
||||
virtual_mailbox_domains: "{{ postfix_mail_domains }}"
|
||||
smtpd_sasl_path: private/auth
|
||||
smtpd_sasl_type: dovecot
|
||||
smtpd_sasl_auth_enable: "{{ 'yes' if postfix_enable_smtpd_auth else 'no' }}"
|
||||
smtpd_tls_cert_file: "{{ postfix_tls_certificate }}"
|
||||
smtpd_tls_key_file: "{{ postfix_tls_private_key }}"
|
||||
smtp_tls_security_level: "{{postfix_incoming_tls_security}}"
|
||||
smtpd_tls_security_level: "{{postfix_outgoing_tls_security}}"
|
||||
smtpd_tls_auth_only: "{{ 'yes' if postfix_allow_insecure_auth else 'no'}}"
|
||||
smtpd_tls_session_cache_database: "{{ 'btree:${data_directory}/smtpd_scache' if postfix_tls_session_cache else '' }}"
|
||||
smtpd_client_restrictions: "{{ postfix_client_restrictions }}"
|
||||
smtpd_data_restrictions: "{{ postfix_data_restrictions }}"
|
||||
smtpd_helo_restrictions: "{{ postfix_helo_restrictions }}"
|
||||
smtpd_relay_restrictions: "{{ postfix_relay_restrictions }}"
|
||||
smtpd_recipient_restrictions: "{{ postfix_recipient_restrictions }}"
|
||||
message_size_limit: "{{ postfix_message_size_limit }}"
|
||||
smtpd_helo_required: "{{ 'yes' if postfix_helo_required else 'no' }}"
|
||||
biff: "{{ 'yes' if postfix_biff else 'no' }}"
|
||||
notify: reload postfix
|
||||
|
||||
- name: "Enable submission service"
|
||||
postconf:
|
||||
service: submission
|
||||
type: inet
|
||||
private: 'n'
|
||||
command: smtpd
|
||||
parameter:
|
||||
milter_macro_daemon_name: ORIGINATING
|
||||
smtpd_client_restrictions:
|
||||
- permit_sasl_authenticated
|
||||
- reject
|
||||
smtpd_sasl_auth_enable: 'yes'
|
||||
smtpd_tls_security_level: encrypt
|
||||
syslog_name: postfix/submission
|
||||
notify: reload postfix
|
||||
when: "postfix_submission_enable == True"
|
||||
|
||||
- name: "Disable submission service"
|
||||
postconf:
|
||||
service: submission
|
||||
type: inet
|
||||
state: absent
|
||||
notify: reload postfix
|
||||
when: "postfix_submission_enable == False"
|
||||
|
||||
- name: "Enable postscreen"
|
||||
include_tasks: postscreen.yml
|
||||
when: "postfix_postscreen_enable == True"
|
||||
|
||||
- name: "Disable postscreen"
|
||||
include_tasks: postscreen_disable.yml
|
||||
when: "postfix_postscreen_enable == False"
|
||||
|
||||
# TODO: mensajes
|
||||
|
||||
# TODO: milter_header_checks
|
Loading…
x
Reference in New Issue
Block a user