commit inicial

This commit is contained in:
Mauro Torrez 2019-06-02 18:35:36 -03:00
commit 16c2f29f06
8 changed files with 1169 additions and 0 deletions

209
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,3 @@
# Postfix Ansible role
Rol de Ansible para configurar el servidor SMTP Postfix.

369
action_plugins/postconf.py Executable file
View 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
View 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
View 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
View 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
View 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
View 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