django-orchestra/orchestra/contrib/mailboxes/backends.py

722 lines
28 KiB
Python
Raw Normal View History

2014-10-09 17:04:12 +00:00
import logging
2015-04-29 10:51:30 +00:00
import os
2015-04-28 16:07:16 +00:00
import re
import textwrap
2014-08-22 15:31:44 +00:00
2014-10-06 14:57:02 +00:00
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
2014-08-22 15:31:44 +00:00
from orchestra.contrib.orchestration import ServiceController
2015-04-05 10:46:24 +00:00
from orchestra.contrib.resources import ServiceMonitor
2014-08-22 15:31:44 +00:00
from . import settings
2015-10-07 11:44:30 +00:00
from .models import Address, Mailbox
2014-08-22 15:31:44 +00:00
2014-10-06 14:57:02 +00:00
2014-10-09 17:04:12 +00:00
logger = logging.getLogger(__name__)
2015-12-02 18:53:20 +00:00
class SieveFilteringMixin:
def generate_filter(self, mailbox, context):
name, content = mailbox.get_filtering()
2015-04-28 16:07:16 +00:00
for box in re.findall(r'fileinto\s+"([^"]+)"', content):
2015-04-29 10:51:30 +00:00
# create mailboxes if fileinfo is provided witout ':create' option
2015-04-28 16:07:16 +00:00
context['box'] = box
self.append(textwrap.dedent("""
# Create %(box)s mailbox
2016-06-17 10:00:04 +00:00
su - %(user)s --shell /bin/bash << 'EOF'
mkdir -p "%(maildir)s/.%(box)s"
EOF
2015-09-20 11:35:22 +00:00
if ! grep '%(box)s' %(maildir)s/subscriptions > /dev/null; then
2015-04-29 10:51:30 +00:00
echo '%(box)s' >> %(maildir)s/subscriptions
2015-08-31 11:58:59 +00:00
chown %(user)s:%(user)s %(maildir)s/subscriptions
2015-04-29 10:51:30 +00:00
fi
""") % context
)
context['filtering_path'] = settings.MAILBOXES_SIEVE_PATH % context
context['filtering_cpath'] = re.sub(r'\.sieve$', '.svbin', context['filtering_path'])
if content:
2015-04-29 10:51:30 +00:00
context['filtering'] = ('# %(banner)s\n' + content) % context
self.append(textwrap.dedent("""\
# Create and compile orchestra sieve filtering
2016-06-17 10:00:04 +00:00
su - %(user)s --shell /bin/bash << 'EOF'
mkdir -p $(dirname "%(filtering_path)s")
cat << ' EOF' > %(filtering_path)s
%(filtering)s
EOF
sievec %(filtering_path)s
EOF
2015-04-29 10:51:30 +00:00
""") % context
)
else:
self.append("echo '' > %(filtering_path)s" % context)
2015-08-31 11:58:59 +00:00
self.append('chown %(user)s:%(group)s %(filtering_path)s' % context)
2016-03-08 10:16:49 +00:00
class UNIXUserMaildirController(SieveFilteringMixin, ServiceController):
2015-04-23 19:46:23 +00:00
"""
Assumes that all system users on this servers all mail accounts.
2015-05-12 12:38:40 +00:00
If you want to have system users AND mailboxes on the same server you should consider using virtual mailboxes.
Supports quota allocation via <tt>resources.disk.allocated</tt>.
2015-04-23 19:46:23 +00:00
"""
2015-04-29 10:51:30 +00:00
SHELL = '/dev/null'
2015-04-05 18:02:36 +00:00
verbose_name = _("UNIX maildir user")
2015-03-10 21:51:10 +00:00
model = 'mailboxes.Mailbox'
def save(self, mailbox):
context = self.get_context(mailbox)
self.append(textwrap.dedent("""
# Update/create %(user)s user state
2015-09-20 11:35:22 +00:00
if id %(user)s ; then
if [[ "%(changepass)s" == "True" ]]; then
old_password=$(getent shadow %(user)s | cut -d':' -f2)
usermod %(user)s \\
--shell %(initial_shell)s \\
--password '%(password)s'
if [[ "$old_password" != '%(password)s' ]]; then
# Postfix SASL caches passwords
RESTART_POSTFIX=1
fi
fi
2015-03-10 21:51:10 +00:00
else
useradd %(user)s \\
--home %(home)s \\
--password '%(password)s'
2015-03-10 21:51:10 +00:00
fi
mkdir -p %(home)s
chmod 751 %(home)s
chown %(user)s:%(group)s %(home)s""") % context
)
if hasattr(mailbox, 'resources') and hasattr(mailbox.resources, 'disk'):
self.set_quota(mailbox, context)
self.generate_filter(mailbox, context)
2015-03-10 21:51:10 +00:00
def set_quota(self, mailbox, context):
2015-05-22 13:15:06 +00:00
allocated = mailbox.resources.disk.allocated
scale = mailbox.resources.disk.resource.get_scale()
context['quota'] = allocated * scale
#unit_to_bytes(mailbox.resources.disk.unit)
2015-03-10 21:51:10 +00:00
self.append(textwrap.dedent("""
# Set Maildir quota for %(user)s
2016-06-17 10:00:04 +00:00
su - %(user)s --shell /bin/bash << 'EOF'
mkdir -p %(maildir)s
EOF
2015-09-20 10:57:13 +00:00
if [ ! -f %(maildir)s/maildirsize ]; then
2015-04-29 10:51:30 +00:00
echo "%(quota)iS" > %(maildir)s/maildirsize
chown %(user)s:%(group)s %(maildir)s/maildirsize
2015-03-10 21:51:10 +00:00
else
2015-04-29 10:51:30 +00:00
sed -i '1s/.*/%(quota)iS/' %(maildir)s/maildirsize
2015-03-10 21:51:10 +00:00
fi""") % context
)
def delete(self, mailbox):
context = self.get_context(mailbox)
if context['deleted_home']:
self.append(textwrap.dedent("""\
# Move home into MAILBOXES_MOVE_ON_DELETE_PATH, nesting if exists.
deleted_home="%(deleted_home)s"
while [[ -e $deleted_home ]]; do
deleted_home="${deleted_home}/$(basename ${deleted_home})"
done
mv %(home)s $deleted_home || exit_code=$?
""") % context
)
else:
2016-06-17 10:00:04 +00:00
self.append("rm -fr -- %(base_home)s" % context)
2015-03-10 21:51:10 +00:00
self.append(textwrap.dedent("""
2015-05-12 12:38:40 +00:00
nohup bash -c '{ sleep 2 && killall -u %(user)s -s KILL; }' &> /dev/null &
2015-03-10 21:51:10 +00:00
killall -u %(user)s || true
2015-12-02 18:53:20 +00:00
# Restart because of Postfix SASL caching credentials
userdel %(user)s && RESTART_POSTFIX=1 || true
2015-03-10 21:51:10 +00:00
groupdel %(user)s || true""") % context
)
2015-05-19 13:27:04 +00:00
def commit(self):
self.append('[[ $RESTART_POSTFIX -eq 1 ]] && service postfix restart')
2015-12-02 18:53:20 +00:00
super().commit()
2015-05-19 13:27:04 +00:00
2015-03-10 21:51:10 +00:00
def get_context(self, mailbox):
# Check if you have to change password
try:
changepass = mailbox.changepass
except:
changepass = True
2015-03-10 21:51:10 +00:00
context = {
'user': mailbox.name,
'group': mailbox.name,
'name': mailbox.name,
2015-03-10 21:51:10 +00:00
'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
'home': mailbox.get_home(),
2015-04-29 10:51:30 +00:00
'maildir': os.path.join(mailbox.get_home(), 'Maildir'),
'initial_shell': self.SHELL,
'banner': self.get_banner(),
'changepass': changepass,
2015-03-10 21:51:10 +00:00
}
context['deleted_home'] = settings.MAILBOXES_MOVE_ON_DELETE_PATH % context
return context
2015-03-10 21:51:10 +00:00
2016-03-08 10:16:49 +00:00
#class DovecotPostfixPasswdVirtualUserController(SieveFilteringMixin, ServiceController):
2015-10-07 11:44:30 +00:00
# """
# WARNING: This backends is not fully implemented
# """
# DEFAULT_GROUP = 'postfix'
#
# verbose_name = _("Dovecot-Postfix virtualuser")
# model = 'mailboxes.Mailbox'
#
# def set_user(self, context):
# self.append(textwrap.dedent("""
# if grep '^%(user)s:' %(passwd_path)s > /dev/null ; then
# sed -i 's#^%(user)s:.*#%(passwd)s#' %(passwd_path)s
# else
# echo '%(passwd)s' >> %(passwd_path)s
# fi""") % context
# )
# self.append("mkdir -p %(home)s" % context)
# self.append("chown %(uid)s:%(gid)s %(home)s" % context)
#
# def set_mailbox(self, context):
# self.append(textwrap.dedent("""
# if ! grep '^%(user)s@%(mailbox_domain)s\s' %(virtual_mailbox_maps)s > /dev/null; then
# echo "%(user)s@%(mailbox_domain)s\tOK" >> %(virtual_mailbox_maps)s
# UPDATED_VIRTUAL_MAILBOX_MAPS=1
# fi""") % context
# )
#
# def save(self, mailbox):
# context = self.get_context(mailbox)
# self.set_user(context)
# self.set_mailbox(context)
# self.generate_filter(mailbox, context)
#
# def delete(self, mailbox):
# context = self.get_context(mailbox)
# self.append(textwrap.dedent("""
# nohup bash -c 'sleep 2 && killall -u %(uid)s -s KILL' &> /dev/null &
# killall -u %(uid)s || true
# sed -i '/^%(user)s:.*/d' %(passwd_path)s
# sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s
# UPDATED_VIRTUAL_MAILBOX_MAPS=1""") % context
# )
# if context['deleted_home']:
# self.append("mv %(home)s %(deleted_home)s || exit_code=$?" % context)
# else:
2016-06-17 10:00:04 +00:00
# self.append("rm -fr -- %(home)s" % context)
2015-10-07 11:44:30 +00:00
#
# def get_extra_fields(self, mailbox, context):
# context['quota'] = self.get_quota(mailbox)
# return 'userdb_mail=maildir:~/Maildir {quota}'.format(**context)
#
# def get_quota(self, mailbox):
# try:
# quota = mailbox.resources.disk.allocated
# except (AttributeError, ObjectDoesNotExist):
# return ''
# unit = mailbox.resources.disk.unit[0].upper()
# return 'userdb_quota_rule=*:bytes=%i%s' % (quota, unit)
#
# def commit(self):
# context = {
# 'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
# }
# self.append(textwrap.dedent("""
# [[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && {
# postmap %(virtual_mailbox_maps)s
# }""") % context
# )
#
# def get_context(self, mailbox):
# context = {
# 'name': mailbox.name,
# 'user': mailbox.name,
# 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
# 'uid': 10000 + mailbox.pk,
# 'gid': 10000 + mailbox.pk,
# 'group': self.DEFAULT_GROUP,
# 'quota': self.get_quota(mailbox),
# 'passwd_path': settings.MAILBOXES_PASSWD_PATH,
# 'home': mailbox.get_home(),
# 'banner': self.get_banner(),
# 'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH,
# 'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
# }
# context['extra_fields'] = self.get_extra_fields(mailbox, context)
# context.update({
# 'passwd': '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context),
# 'deleted_home': settings.MAILBOXES_MOVE_ON_DELETE_PATH % context,
# })
# return context
2014-08-22 15:31:44 +00:00
2016-03-08 10:16:49 +00:00
class PostfixAddressVirtualDomainController(ServiceController):
2015-04-23 19:46:23 +00:00
"""
2015-05-05 19:42:55 +00:00
Secondary SMTP server without mailboxes in it, only syncs virtual domains.
2015-04-23 19:46:23 +00:00
"""
2015-05-05 19:42:55 +00:00
verbose_name = _("Postfix address virtdomain-only")
2014-10-17 10:04:47 +00:00
model = 'mailboxes.Address'
related_models = (
('mailboxes.Mailbox', 'addresses'),
)
2015-04-24 11:39:20 +00:00
doc_settings = (settings,
2015-05-05 19:42:55 +00:00
('MAILBOXES_LOCAL_DOMAIN', 'MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH')
2015-04-24 11:39:20 +00:00
)
2014-08-22 15:31:44 +00:00
2015-06-22 14:14:16 +00:00
def is_hosted_domain(self, domain):
2015-05-05 19:42:55 +00:00
""" whether or not domain MX points to this server """
return domain.has_default_mx()
2014-10-09 17:04:12 +00:00
2015-05-05 19:42:55 +00:00
def include_virtual_alias_domain(self, context):
domain = context['domain']
2015-06-22 14:14:16 +00:00
if domain.name != context['local_domain'] and self.is_hosted_domain(domain):
2014-10-09 17:04:12 +00:00
self.append(textwrap.dedent("""
# %(domain)s is a virtual domain belonging to this server
2015-09-20 11:35:22 +00:00
if ! grep '^\s*%(domain)s\s*$' %(virtual_alias_domains)s > /dev/null; then
2015-05-05 19:42:55 +00:00
echo '%(domain)s' >> %(virtual_alias_domains)s
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
2015-05-12 12:38:40 +00:00
fi""") % context
2015-05-05 19:42:55 +00:00
)
2014-08-22 15:31:44 +00:00
2015-05-05 19:42:55 +00:00
def is_last_domain(self, domain):
return not Address.objects.filter(domain=domain).exists()
def exclude_virtual_alias_domain(self, context):
domain = context['domain']
if self.is_last_domain(domain):
# Prevent deleting the same domain multiple times on bulk deletes
if not hasattr(self, '_excluded_domains'):
self._excluded_domains = set()
if domain.name not in self._excluded_domains:
self._excluded_domains.add(domain.name)
self.append(textwrap.dedent("""
# Delete %(domain)s virtual domain
if grep '^%(domain)s\s*$' %(virtual_alias_domains)s > /dev/null; then
sed -i '/^%(domain)s\s*/d' %(virtual_alias_domains)s
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
fi""") % context
)
2014-08-22 15:31:44 +00:00
def save(self, address):
context = self.get_context(address)
2014-10-09 17:04:12 +00:00
self.include_virtual_alias_domain(context)
2015-05-05 19:42:55 +00:00
return context
2014-08-22 15:31:44 +00:00
def delete(self, address):
context = self.get_context(address)
2014-10-09 17:04:12 +00:00
self.exclude_virtual_alias_domain(context)
2015-05-05 19:42:55 +00:00
return context
2014-08-22 15:31:44 +00:00
def commit(self):
context = self.get_context_files()
self.append(textwrap.dedent("""
2015-05-05 19:42:55 +00:00
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && {
service postfix reload
}
exit $exit_code
2014-11-27 19:17:26 +00:00
""") % context
)
2014-08-22 15:31:44 +00:00
def get_context_files(self):
return {
2014-10-17 10:04:47 +00:00
'virtual_alias_domains': settings.MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH,
'virtual_alias_maps': settings.MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH
2014-08-22 15:31:44 +00:00
}
def get_context(self, address):
context = self.get_context_files()
context.update({
2015-10-07 11:44:30 +00:00
'name': address.name,
2014-08-22 15:31:44 +00:00
'domain': address.domain,
'email': address.email,
2015-04-09 14:32:10 +00:00
'local_domain': settings.MAILBOXES_LOCAL_DOMAIN,
2014-08-22 15:31:44 +00:00
})
return context
2014-08-22 15:31:44 +00:00
2016-03-08 10:16:49 +00:00
class PostfixAddressController(PostfixAddressVirtualDomainController):
2015-05-05 19:42:55 +00:00
"""
2016-03-08 10:16:49 +00:00
Addresses based on Postfix virtual alias domains, includes <tt>PostfixAddressVirtualDomainController</tt>.
2015-05-05 19:42:55 +00:00
"""
verbose_name = _("Postfix address")
2015-05-22 13:15:06 +00:00
doc_settings = (settings, (
'MAILBOXES_LOCAL_DOMAIN',
'MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH',
'MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH'
))
2015-05-05 19:42:55 +00:00
2015-10-07 11:44:30 +00:00
def is_implicit_entry(self, context):
"""
check if virtual_alias_map entry can be omitted because the address is
equivalent to its local mbox
"""
return bool(
context['domain'].name == context['local_domain'] and
context['destination'] == context['name'] and
Mailbox.objects.filter(name=context['name']).exists())
2015-05-05 19:42:55 +00:00
def update_virtual_alias_maps(self, address, context):
2015-10-07 11:44:30 +00:00
context['destination'] = address.destination
if not self.is_implicit_entry(context):
2015-05-05 19:42:55 +00:00
self.append(textwrap.dedent("""
# Set virtual alias entry for %(email)s
2015-05-05 19:42:55 +00:00
LINE='%(email)s\t%(destination)s'
2015-09-20 11:35:22 +00:00
if ! grep '^%(email)s\s' %(virtual_alias_maps)s > /dev/null; then
2015-05-05 19:42:55 +00:00
# Add new line
echo "${LINE}" >> %(virtual_alias_maps)s
UPDATED_VIRTUAL_ALIAS_MAPS=1
else
# Update existing line, if needed
2015-09-20 11:35:22 +00:00
if ! grep "^${LINE}$" %(virtual_alias_maps)s > /dev/null; then
2015-05-05 19:42:55 +00:00
sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtual_alias_maps)s
UPDATED_VIRTUAL_ALIAS_MAPS=1
fi
fi""") % context)
else:
2015-10-07 11:44:30 +00:00
if not context['destination']:
msg = "Address %i is empty" % address.pk
self.append("\necho 'msg' >&2" % msg)
logger.warning(msg)
else:
self.append("\n# %(email)s %(destination)s entry is redundant" % context)
self.exclude_virtual_alias_maps(context)
2015-05-05 19:42:55 +00:00
# Virtual mailbox stuff
# destination = []
# for mailbox in address.get_mailboxes():
# context['mailbox'] = mailbox
# destination.append("%(mailbox)s@%(local_domain)s" % context)
# for forward in address.forward:
# if '@' in forward:
# destination.append(forward)
def exclude_virtual_alias_maps(self, context):
2015-10-07 11:44:30 +00:00
self.append(textwrap.dedent("""\
# Remove %(email)s virtual alias entry
2015-09-20 11:35:22 +00:00
if grep '^%(email)s\s' %(virtual_alias_maps)s > /dev/null; then
sed -i '/^%(email)s\s/d' %(virtual_alias_maps)s
2015-05-05 19:42:55 +00:00
UPDATED_VIRTUAL_ALIAS_MAPS=1
fi""") % context
)
2015-05-05 19:42:55 +00:00
def save(self, address):
2015-12-02 18:53:20 +00:00
context = super().save(address)
2015-05-05 19:42:55 +00:00
self.update_virtual_alias_maps(address, context)
def delete(self, address):
context = super().delete(address)
2015-05-05 19:42:55 +00:00
self.exclude_virtual_alias_maps(context)
def commit(self):
context = self.get_context_files()
self.append(textwrap.dedent("""
# Apply changes if needed
2015-05-05 19:42:55 +00:00
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && {
service postfix reload
}
[[ $UPDATED_VIRTUAL_ALIAS_MAPS == 1 ]] && {
postmap %(virtual_alias_maps)s
}
exit $exit_code
""") % context
)
2016-03-08 10:16:49 +00:00
class AutoresponseController(ServiceController):
2015-04-23 19:46:23 +00:00
"""
WARNING: not implemented
"""
2014-08-22 15:31:44 +00:00
verbose_name = _("Mail autoresponse")
2015-04-05 18:02:36 +00:00
model = 'mailboxes.Autoresponse'
2014-08-22 15:31:44 +00:00
2015-04-05 18:02:36 +00:00
class DovecotMaildirDisk(ServiceMonitor):
2014-11-18 13:59:21 +00:00
"""
Maildir disk usage based on Dovecot maildirsize file
http://wiki2.dovecot.org/Quota/Maildir
"""
2014-10-17 10:04:47 +00:00
model = 'mailboxes.Mailbox'
2014-08-22 15:31:44 +00:00
resource = ServiceMonitor.DISK
2015-04-05 18:02:36 +00:00
verbose_name = _("Dovecot Maildir size")
delete_old_equal_values = True
2015-04-24 11:39:20 +00:00
doc_settings = (settings,
('MAILBOXES_MAILDIRSIZE_PATH',)
)
2014-08-22 15:31:44 +00:00
2014-11-18 13:59:21 +00:00
def prepare(self):
2015-12-02 18:53:20 +00:00
super().prepare()
2014-11-18 13:59:21 +00:00
current_date = self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z")
# self.append(textwrap.dedent("""\
# function monitor () {
# awk 'BEGIN { size = 0 } NR > 1 { size += $1 } END { print size }' $1 || echo 0
# }"""))
2014-11-18 13:59:21 +00:00
self.append(textwrap.dedent("""\
function monitor () {
SIZE=$(du -sb $1/Maildir/ 2> /dev/null || echo 0) && echo $SIZE | awk '{print $1}'
2024-02-09 06:13:51 +00:00
list=()
2014-11-18 13:59:21 +00:00
}"""))
2014-08-22 15:31:44 +00:00
def monitor(self, mailbox):
context = self.get_context(mailbox)
# self.append("echo %(object_id)s $(monitor %(maildir_path)s)" % context)
2024-02-09 06:13:51 +00:00
# self.append("echo %(object_id)s $(monitor %(home)s)" % context)
self.append("list[${#list[@]}]=\'echo %(object_id)s $(monitor %(home)s)\'" % context)
def commit(self):
self.append(textwrap.dedent("""\
proces=0
for cmd in "${list[@]}"
do
eval $cmd &
proces=$((proces+1))
if [ $proces -ge 10 ];then
wait
proces=0
fi
done
wait
exit $exit_code
"""))
2014-08-22 15:31:44 +00:00
def get_context(self, mailbox):
2014-10-23 21:25:44 +00:00
context = {
2014-11-17 14:17:33 +00:00
'home': mailbox.get_home(),
2014-10-06 14:57:02 +00:00
'object_id': mailbox.pk
2014-10-23 21:25:44 +00:00
}
2014-11-17 14:17:33 +00:00
context['maildir_path'] = settings.MAILBOXES_MAILDIRSIZE_PATH % context
return context
2015-04-05 18:02:36 +00:00
class PostfixMailscannerTraffic(ServiceMonitor):
"""
2015-04-23 19:46:23 +00:00
A high-performance log parser.
Reads the mail.log file only once, for all users.
"""
model = 'mailboxes.Mailbox'
resource = ServiceMonitor.TRAFFIC
2015-04-05 18:02:36 +00:00
verbose_name = _("Postfix-Mailscanner traffic")
2024-03-08 19:37:19 +00:00
script_executable = '/usr/bin/python3'
monthly_sum_old_values = True
2015-04-24 11:39:20 +00:00
doc_settings = (settings,
('MAILBOXES_MAIL_LOG_PATH',)
)
def prepare(self):
2015-04-05 18:02:36 +00:00
mail_log = settings.MAILBOXES_MAIL_LOG_PATH
context = {
'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
'mail_logs': str((mail_log, mail_log+'.1')),
}
self.append(textwrap.dedent("""\
import re
import sys
from datetime import datetime
from dateutil import tz
def to_local_timezone(date, tzlocal=tz.tzlocal()):
# Converts orchestra's UTC dates to local timezone
date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S %Z')
date = date.replace(tzinfo=tz.tzutc())
date = date.astimezone(tzlocal)
return date
maillogs = {mail_logs}
end_datetime = to_local_timezone('{current_date}')
end_date = int(end_datetime.strftime('%Y%m%d%H%M%S'))
2015-04-29 10:51:30 +00:00
months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
months = dict((m, '%02d' % n) for n, m in enumerate(months, 1))
2024-03-11 17:31:07 +00:00
users = {{}}
sends = {{}}
register_imap_traffic = False
register_pop_traffic = False
def inside_period(month, day, time, ini_date):
global months
global end_datetime
2015-04-09 14:32:10 +00:00
# Mar 9 17:13:22
month = months[month]
year = end_datetime.year
if month == '12' and end_datetime.month == 1:
year = year+1
2015-04-09 14:32:10 +00:00
if len(day) == 1:
day = '0' + day
date = str(year) + month + day
date += time.replace(':', '')
return ini_date < int(date) < end_date
2024-03-11 17:31:07 +00:00
def search_username(pattern, users, line):
match = pattern.search(line)
if not match:
return None
username = match.groups(1)[0]
if username not in users.keys():
return None
return username
def search_size(line, users, username, pattern):
month, day, time, req_id = line.split()[:4]
if inside_period(month, day, time, users[username][0]):
group = req_id.split('<')[-1][:-2]
matches = pattern.search(line)
if not matches:
return None, None
return group, matches
return None, None
def prepare(object_id, mailbox, ini_date):
global users
2024-03-08 19:37:19 +00:00
global sends
ini_date = to_local_timezone(ini_date)
ini_date = int(ini_date.strftime('%Y%m%d%H%M%S'))
users[mailbox] = (ini_date, object_id)
2024-03-08 19:37:19 +00:00
sends[mailbox] = {{}}
2024-03-08 19:37:19 +00:00
def monitor(users, sends, maillogs):
grupos = []
sasl_username_pattern = re.compile(r'sasl_username=([a-zA-Z0-9\.\-_]+)')
size_pattern = re.compile(r'size=(\d+),')
2024-03-11 17:31:07 +00:00
pop_username_pattern = re.compile(r' pop3\(([^)].*)\)')
pop_size_pattern = re.compile(r'size=(\d+)')
imap_username_pattern = re.compile(r' imap\(([^)].*)\)')
imap_size_pattern = re.compile(r"in=(\d+) out=(\d+)")
for maillog in maillogs:
try:
with open(maillog, 'r') as maillog:
for line in maillog.readlines():
# Only search for Authenticated sendings
2024-03-08 19:37:19 +00:00
if 'sasl_username=' in line:
# si el usuario es uno de los elegidos y el rango de tiempo es correcto
# recoge el id de grupo
2024-03-11 17:31:07 +00:00
username = search_username(sasl_username_pattern, users, line)
if username is None:
continue
2024-03-08 19:37:19 +00:00
month, day, time, __, __, req_id = line.split()[:6]
if inside_period(month, day, time, users[username][0]):
group = req_id[:-1]
sends[username][group] = 0
grupos.append(group)
else:
# busca el size de envios donde se alla anadido el groupID anteriormente,
# una vez encontrado borra el groupID
for id in grupos:
if id in line:
match = size_pattern.search(line)
if not match:
continue
for k, v in sends.items():
if id in sends[k].keys():
sends[k][id] += int(match.groups(1)[0])
grupos.remove(id)
2024-03-11 17:31:07 +00:00
# pop trafic
if register_pop_traffic:
if 'pop3(' in line and 'size' in line:
username = search_username(pop_username_pattern, users, line)
if username is None:
continue
group, matches = search_size(line, users, username, pop_size_pattern)
if group is not None and matches is not None :
sends[username][group] = int(matches.groups(1)[0])
# imap trafic
if register_imap_traffic:
if 'imap(' in line and 'out=' in line:
username = search_username(imap_username_pattern, users, line)
if username is None:
continue
group, matches = search_size(line, users, username, imap_size_pattern)
if group is not None and matches is not None :
value = int(matches.group(1)) + int(matches.group(2))
sends[username][group] = value
except IOError as e:
sys.stderr.write(str(e)+'\\n')
2024-03-08 19:37:19 +00:00
# devolver la sumatoria de valores a orchestra (id_user, size)
for username, opts in users.items():
total_size = 0
for size in sends[username].values():
total_size += size
print(f"{{opts[1]}} {{total_size}}")
""").format(**context)
)
def commit(self):
2024-03-08 19:37:19 +00:00
self.append('monitor(users, sends, maillogs)')
def monitor(self, mailbox):
context = self.get_context(mailbox)
self.append("prepare(%(object_id)s, '%(mailbox)s', '%(last_date)s')" % context)
def get_context(self, mailbox):
2015-04-05 18:02:36 +00:00
context = {
'mailbox': mailbox.name,
'object_id': mailbox.pk,
'last_date': self.get_last_date(mailbox.pk).strftime("%Y-%m-%d %H:%M:%S %Z"),
}
return context
class RoundcubeIdentityController(ServiceController):
"""
WARNING: not implemented
"""
verbose_name = _("Roundcube Identity Controller")
model = 'mailboxes.Mailbox'
2024-06-27 16:08:46 +00:00
class RSpamdRatelimitController(ServiceController):
"""
rspamd ratelimit to user
"""
verbose_name = _("rspamd ratelimit user")
model = 'mailboxes.Mailbox'
def save(self, mailbox):
context = self.get_context(mailbox)
self.append(textwrap.dedent("""
2024-07-09 15:36:32 +00:00
sed -i '/^%(user)s$/d' %(maps)s
echo '%(user)s' >> %(path_maps)s%(ratelimit)s.map
systemctl reload rspamd.service
2024-06-27 16:08:46 +00:00
""") % context
)
def delete(self, mailbox):
context = self.get_context(mailbox)
self.append(textwrap.dedent("""
2024-07-09 15:36:32 +00:00
sed -i '/^%(user)s$/d' %(maps)s
systemctl reload rspamd.service
2024-06-27 16:08:46 +00:00
""") % context
)
2024-07-09 15:36:32 +00:00
# def commit(self):
# self.append('[[ $RELOAD_RSPAMD -eq 1 ]] && systemctl reload rspamd.service')
# super().commit()
2024-07-08 16:30:00 +00:00
2024-06-27 16:08:46 +00:00
def get_context(self, mailbox):
2024-07-08 16:30:00 +00:00
maps = self.extract_group_maps()
2024-06-27 16:08:46 +00:00
context = {
'user': mailbox.name,
2024-07-08 16:30:00 +00:00
'ratelimit': mailbox.ratelimit,
'maps': maps,
'path_maps': settings.MAILBOXES_RATELIMIT_PATH_MAPS,
2024-06-27 16:08:46 +00:00
}
2024-07-08 16:30:00 +00:00
return context
def extract_group_maps(self):
"""
debulve string de todos los ficheros de maps assignados en settings para ratelimit
return string
"""
choice_groups = settings.MAILBOXES_RATELIMIT_GROUP
path = settings.MAILBOXES_RATELIMIT_PATH_MAPS
group_maps = ''
if len(choice_groups) > 0:
for choice in choice_groups:
group_maps += f"{path}{choice[0]}.map "
return group_maps