django-orchestra/orchestra/apps/mails/backends.py

205 lines
7.5 KiB
Python

import textwrap
import os
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from . import settings
from .models import Address
# TODO http://wiki2.dovecot.org/HowTo/SimpleVirtualInstall
# TODO http://wiki2.dovecot.org/HowTo/VirtualUserFlatFilesPostfix
# TODO Set first/last_valid_uid/gid settings to contain only the range actually used by mail processes
# TODO Insert "/./" inside the returned home directory, eg.: home=/home/./user to chroot into /home, or home=/home/user/./ to chroot into /home/user.
# TODO mount the filesystem with "nosuid" option
class PasswdVirtualUserBackend(ServiceController):
verbose_name = _("Mail virtual user (passwd-file)")
model = 'mails.Mailbox'
# TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data
DEFAULT_GROUP = 'postfix'
def set_user(self, context):
self.append(textwrap.dedent("""
if [[ $( grep "^%(username)s:" %(passwd_path)s ) ]]; then
sed -i "s/^%(username)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 generate_filter(self, mailbox, context):
now = timezone.now().strftime("%B %d, %Y, %H:%M")
context['filtering'] = (
"# Sieve Filter\n"
"# Generated by Orchestra %s\n\n" % now
)
if mailbox.custom_filtering:
context['filtering'] += mailbox.custom_filtering
else:
context['filtering'] += settings.MAILS_DEFAUL_FILTERING
context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve')
self.append("echo '%(filtering)s' > %(filter_path)s" % context)
def set_quota(self, mailbox, context):
if not hasattr(mailbox, 'resources'):
return
context.update({
'maildir_path': '~%(username)s/Maildir' % context,
'maildirsize_path': '~%(username)s/Maildir/maildirsize' % context,
'quota': mailbox.resources.disk.allocated*1000*1000,
})
self.append("mkdir -p %(maildir_path)s" % context)
self.append(textwrap.dedent("""
sed -i '1s/.*/%(quota)s,S/' %(maildirsize_path)s || {
echo '%(quota)s,S' > %(maildirsize_path)s &&
chown %(username)s %(maildirsize_path)s;
}""" % context
))
def save(self, mailbox):
context = self.get_context(mailbox)
self.set_user(context)
self.generate_filter(mailbox, context)
def delete(self, mailbox):
context = self.get_context(mailbox)
self.append("{ sleep 2 && killall -u %(uid)s -s KILL; } &" % context)
self.append("killall -u %(uid)s" % context)
self.append("sed -i '/^%(username)s:.*/d' %(passwd_path)s" % context)
self.append("rm -fr %(home)s" % context)
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 get_context(self, mailbox):
context = {
'name': mailbox.name,
'username': 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.MAILS_PASSWD_PATH,
'home': mailbox.get_home(),
}
context['extra_fields'] = self.get_extra_fields(mailbox, context)
context['passwd'] = '{username}:{password}:{uid}:{gid}:,,,:{home}:{extra_fields}'.format(**context)
return context
class PostfixAddressBackend(ServiceController):
verbose_name = _("Postfix address")
model = 'mails.Address'
def include_virtdomain(self, context):
self.append(
'[[ $(grep "^\s*%(domain)s\s*$" %(virtdomains)s) ]]'
' || { echo "%(domain)s" >> %(virtdomains)s; UPDATED_VIRTDOMAINS=1; }' % context
)
def exclude_virtdomain(self, context):
domain = context['domain']
if not Address.objects.filter(domain=domain).exists():
self.append('sed -i "s/^%(domain)s//" %(virtdomains)s' % context)
def update_virtusertable(self, context):
self.append(textwrap.dedent("""
LINE="%(email)s\t%(destination)s"
if [[ ! $(grep "^%(email)s\s" %(virtusertable)s) ]]; then
echo "${LINE}" >> %(virtusertable)s
UPDATED_VIRTUSERTABLE=1
else
if [[ ! $(grep "^${LINE}$" %(virtusertable)s) ]]; then
sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtusertable)s
UPDATED_VIRTUSERTABLE=1
fi
fi""" % context
))
def exclude_virtusertable(self, context):
self.append(textwrap.dedent("""
if [[ $(grep "^%(email)s\s") ]]; then
sed -i "s/^%(email)s\s.*$//" %(virtusertable)s
UPDATED=1
fi"""
))
def save(self, address):
context = self.get_context(address)
self.include_virtdomain(context)
self.update_virtusertable(context)
def delete(self, address):
context = self.get_context(address)
self.exclude_virtdomain(context)
self.exclude_virtusertable(context)
def commit(self):
context = self.get_context_files()
self.append(textwrap.dedent("""
[[ $UPDATED_VIRTUSERTABLE == 1 ]] && { postmap %(virtusertable)s; }
# TODO not sure if always needed
[[ $UPDATED_VIRTDOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
""" % context
))
def get_context_files(self):
return {
'virtdomains': settings.MAILS_VIRTDOMAINS_PATH,
'virtusertable': settings.MAILS_VIRTUSERTABLE_PATH,
}
def get_context(self, address):
context = self.get_context_files()
context.update({
'domain': address.domain,
'email': address.email,
'destination': address.destination,
})
return context
class AutoresponseBackend(ServiceController):
verbose_name = _("Mail autoresponse")
model = 'mail.Autoresponse'
class MaildirDisk(ServiceMonitor):
model = 'mails.Mailbox'
resource = ServiceMonitor.DISK
verbose_name = _("Maildir disk usage")
def monitor(self, mailbox):
context = self.get_context(mailbox)
self.append(
"SIZE=$(sed -n '2p' %(maildir_path)s | cut -d' ' -f1)\n"
"echo %(object_id)s ${SIZE:-0}" % context
)
def get_context(self, mailbox):
context = MailSystemUserBackend().get_context(mailbox)
context.update({
'rr_path': os.path.join(context['home'], 'Maildir/maildirsize'),
'object_id': mailbox.pk
})
return context