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 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 || true" % context) self.append("sed -i '/^%(username)s:.*/d' %(passwd_path)s" % context) # TODO delete context['deleted'] = context['home'].rstrip('/') + '.deleted' self.append("mv %(home)s %(deleted)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