diff --git a/orchestra/apps/users/backends.py b/orchestra/apps/users/backends.py
new file mode 100644
index 00000000..1af71f40
--- /dev/null
+++ b/orchestra/apps/users/backends.py
@@ -0,0 +1,123 @@
+import textwrap
+
+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
+
+
+class SystemUserBackend(ServiceController):
+ verbose_name = _("System User")
+ model = 'users.User'
+ ignore_fields = ['last_login']
+
+ def save(self, user):
+ context = self.get_context(user)
+ if user.is_main:
+ self.append(textwrap.dedent("""
+ if [[ $( id %(username)s ) ]]; then
+ usermod --password '%(password)s' %(username)s
+ else
+ useradd %(username)s --password '%(password)s' \\
+ --shell /bin/false
+ fi
+ mkdir -p %(home)s
+ chown %(username)s.%(username)s %(home)s""" % context
+ ))
+ else:
+ self.delete(user)
+
+ def delete(self, user):
+ context = self.get_context(user)
+ self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context)
+ self.append("killall -u %(username)s" % context)
+ self.append("userdel %(username)s" % context)
+
+ def get_context(self, user):
+ context = {
+ 'username': user.username,
+ 'password': user.password if user.is_active else '*%s' % user.password,
+ }
+ context['home'] = settings.USERS_SYSTEMUSER_HOME % context
+ return context
+
+
+class SystemUserDisk(ServiceMonitor):
+ model = 'users.User'
+ resource = ServiceMonitor.DISK
+ verbose_name = _('System user disk')
+
+ def monitor(self, user):
+ context = self.get_context(user)
+ self.append("du -s %(home)s | xargs echo %(object_id)s" % context)
+
+ def get_context(self, user):
+ context = SystemUserBackend().get_context(user)
+ context['object_id'] = user.pk
+ return context
+
+
+class FTPTraffic(ServiceMonitor):
+ model = 'users.User'
+ resource = ServiceMonitor.TRAFFIC
+ verbose_name = _('FTP traffic')
+
+ def prepare(self):
+ current_date = timezone.localtime(self.current_date)
+ current_date = current_date.strftime("%Y%m%d%H%M%S")
+ self.append(textwrap.dedent("""
+ function monitor () {
+ OBJECT_ID=$1
+ INI_DATE=$2
+ USERNAME="$3"
+ LOG_FILE="$4"
+ grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \\
+ | grep " \\[${USERNAME}\\] " \\
+ | awk -v ini="${INI_DATE}" '
+ BEGIN {
+ end = "%s"
+ sum = 0
+ months["Jan"] = "01"
+ months["Feb"] = "02"
+ months["Mar"] = "03"
+ months["Apr"] = "04"
+ months["May"] = "05"
+ months["Jun"] = "06"
+ months["Jul"] = "07"
+ months["Aug"] = "08"
+ months["Sep"] = "09"
+ months["Oct"] = "10"
+ months["Nov"] = "11"
+ months["Dec"] = "12"
+ } {
+ # log: Fri Jul 11 13:23:17 2014
+ split($4, t, ":")
+ # line_date = year month day hour minute second
+ line_date = $5 months[$2] $3 t[1] t[2] t[3]
+ if ( line_date > ini && line_date < end)
+ split($0, l, "\\", ")
+ split(l[3], b, " ")
+ sum += b[1]
+ } END {
+ print sum
+ }
+ ' | xargs echo ${OBJECT_ID}
+ }""" % current_date))
+
+ def monitor(self, user):
+ context = self.get_context(user)
+ self.append(
+ 'monitor %(object_id)i %(last_date)s "%(username)s" "%(log_file)s"' % context)
+
+ def get_context(self, user):
+ last_date = timezone.localtime(self.get_last_date(user.pk))
+ return {
+ 'log_file': settings.USERS_FTP_LOG_PATH,
+ 'last_date': last_date.strftime("%Y%m%d%H%M%S"),
+ 'object_id': user.pk,
+ 'username': user.username,
+ }
+
diff --git a/orchestra/apps/users/roles/__init__.py b/orchestra/apps/users/roles/__init__.py
new file mode 100644
index 00000000..62b9ee6f
--- /dev/null
+++ b/orchestra/apps/users/roles/__init__.py
@@ -0,0 +1,27 @@
+from ..models import User
+
+
+class Register(object):
+ def __init__(self):
+ self._registry = {}
+
+ def __contains__(self, key):
+ return key in self._registry
+
+ def register(self, name, model):
+ if name in self._registry:
+ raise KeyError("%s already registered" % name)
+ def has_role(user, model=model):
+ try:
+ getattr(user, name)
+ except model.DoesNotExist:
+ return False
+ return True
+ setattr(User, 'has_%s' % name, has_role)
+ self._registry[name] = model
+
+ def get(self):
+ return self._registry
+
+
+roles = Register()
diff --git a/orchestra/apps/users/roles/admin.py b/orchestra/apps/users/roles/admin.py
new file mode 100644
index 00000000..0432a682
--- /dev/null
+++ b/orchestra/apps/users/roles/admin.py
@@ -0,0 +1,145 @@
+from django.contrib import messages
+from django.contrib.admin.util import unquote, get_deleted_objects
+from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
+from django.db import router
+from django.http import Http404, HttpResponseRedirect
+from django.template.response import TemplateResponse
+from django.shortcuts import redirect
+from django.utils.encoding import force_text
+from django.utils.html import escape
+from django.utils.translation import ugettext, ugettext_lazy as _
+
+from orchestra.admin.utils import get_modeladmin, change_url
+
+from .forms import role_form_factory
+from ..models import User
+
+
+class RoleAdmin(object):
+ model = None
+ name = ''
+ url_name = ''
+ form = None
+
+ def __init__(self, user=None):
+ self.user = user
+
+ @property
+ def exists(self):
+ try:
+ return getattr(self.user, self.name)
+ except self.model.DoesNotExist:
+ return False
+
+ def get_user(self, request, object_id):
+ try:
+ user = User.objects.get(pk=unquote(object_id))
+ except User.DoesNotExist:
+ opts = self.model._meta
+ raise Http404(
+ _('%(name)s object with primary key %(key)r does not exist.') %
+ {'name': force_text(opts.verbose_name), 'key': escape(object_id)}
+ )
+ return user
+
+ def change_view(self, request, object_id):
+ modeladmin = get_modeladmin(User)
+ user = self.get_user(request, object_id)
+ self.user = user
+ obj = None
+ exists = self.exists
+ if exists:
+ obj = getattr(user, self.name)
+ form_class = self.form if self.form else role_form_factory(self)
+ form = form_class(instance=obj)
+ opts = User._meta
+ app_label = opts.app_label
+ title = _("Add %s for user %s" % (self.name, user))
+ action = _("Create")
+ # User has submitted the form
+ if request.method == 'POST':
+ form = form_class(request.POST, instance=obj)
+ form.user = user
+ if form.is_valid():
+ obj = form.save()
+ context = {
+ 'name': obj._meta.verbose_name,
+ 'obj': obj,
+ 'action': _("saved" if exists else "created")
+ }
+ modeladmin.log_change(request, request.user, "%s saved" % self.name.capitalize())
+ msg = _('The role %(name)s for user "%(obj)s" was %(action)s successfully.') % context
+ modeladmin.message_user(request, msg, messages.SUCCESS)
+ if not "_continue" in request.POST:
+ return redirect(change_url(user))
+ exists = True
+
+ if exists:
+ title = _("Change %s %s settings" % (user, self.name))
+ action = _("Save")
+ form = form_class(instance=obj)
+
+ context = {
+ 'title': title,
+ 'opts': opts,
+ 'app_label': app_label,
+ 'form': form,
+ 'action': action,
+ 'role': self,
+ 'roles': [ role(user=user) for role in modeladmin.roles ],
+ 'media': modeladmin.media
+ }
+
+ template = 'admin/users/user/role.html'
+ app = modeladmin.admin_site.name
+ return TemplateResponse(request, template, context, current_app=app)
+
+ def delete_view(self, request, object_id):
+ "The 'delete' admin view for this model."
+ opts = self.model._meta
+ app_label = opts.app_label
+ modeladmin = get_modeladmin(User)
+ user = self.get_user(request, object_id)
+ obj = getattr(user, self.name)
+
+ using = router.db_for_write(self.model)
+
+ # Populate deleted_objects, a data structure of all related objects that
+ # will also be deleted.
+ (deleted_objects, perms_needed, protected) = get_deleted_objects(
+ [obj], opts, request.user, modeladmin.admin_site, using)
+
+ if request.POST: # The user has already confirmed the deletion.
+ if perms_needed:
+ raise PermissionDenied
+ obj_display = force_text(obj)
+ modeladmin.log_deletion(request, obj, obj_display)
+ modeladmin.delete_model(request, obj)
+ post_url = change_url(user)
+ preserved_filters = modeladmin.get_preserved_filters(request)
+ post_url = add_preserved_filters(
+ {'preserved_filters': preserved_filters, 'opts': opts}, post_url
+ )
+ return HttpResponseRedirect(post_url)
+
+ object_name = force_text(opts.verbose_name)
+
+ if perms_needed or protected:
+ title = _("Cannot delete %(name)s") % {"name": object_name}
+ else:
+ title = _("Are you sure?")
+
+ context = {
+ "title": title,
+ "object_name": object_name,
+ "object": obj,
+ "deleted_objects": deleted_objects,
+ "perms_lacking": perms_needed,
+ "protected": protected,
+ "opts": opts,
+ "app_label": app_label,
+ 'preserved_filters': modeladmin.get_preserved_filters(request),
+ 'role': self,
+ }
+ return TemplateResponse(request, 'admin/users/user/delete_role.html',
+ context, current_app=modeladmin.admin_site.name)
diff --git a/orchestra/apps/users/roles/filters.py b/orchestra/apps/users/roles/filters.py
new file mode 100644
index 00000000..7bac9a3b
--- /dev/null
+++ b/orchestra/apps/users/roles/filters.py
@@ -0,0 +1,23 @@
+from django.contrib.admin import SimpleListFilter
+from django.utils.translation import ugettext_lazy as _
+
+
+def role_list_filter_factory(role):
+ class RoleListFilter(SimpleListFilter):
+ """ Filter Nodes by group according to request.user """
+ title = _("has %s" % role.name)
+ parameter_name = role.url_name
+
+ def lookups(self, request, model_admin):
+ return (
+ ('True', _("Yes")),
+ ('False', _("No")),
+ )
+
+ def queryset(self, request, queryset):
+ if self.value() == 'True':
+ return queryset.filter(**{ '%s__isnull' % role.name: False })
+ if self.value() == 'False':
+ return queryset.filter(**{ '%s__isnull' % role.name: True })
+
+ return RoleListFilter
diff --git a/orchestra/apps/users/roles/forms.py b/orchestra/apps/users/roles/forms.py
new file mode 100644
index 00000000..decc6610
--- /dev/null
+++ b/orchestra/apps/users/roles/forms.py
@@ -0,0 +1,17 @@
+from django import forms
+
+
+class RoleAdminBaseForm(forms.ModelForm):
+ class Meta:
+ exclude = ('user', )
+
+ def save(self, *args, **kwargs):
+ self.instance.user = self.user
+ return super(RoleAdminBaseForm, self).save(*args, **kwargs)
+
+
+def role_form_factory(role):
+ class RoleAdminForm(RoleAdminBaseForm):
+ class Meta(RoleAdminBaseForm.Meta):
+ model = role.model
+ return RoleAdminForm
diff --git a/orchestra/apps/users/roles/jabber/__init__.py b/orchestra/apps/users/roles/jabber/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/users/roles/jabber/admin.py b/orchestra/apps/users/roles/jabber/admin.py
new file mode 100644
index 00000000..ba126c0f
--- /dev/null
+++ b/orchestra/apps/users/roles/jabber/admin.py
@@ -0,0 +1,15 @@
+from django.contrib.auth import get_user_model
+
+from orchestra.admin.utils import insertattr
+from orchestra.apps.users.roles.admin import RoleAdmin
+
+from .models import Jabber
+
+
+class JabberRoleAdmin(RoleAdmin):
+ model = Jabber
+ name = 'jabber'
+ url_name = 'jabber'
+
+
+insertattr(get_user_model(), 'roles', JabberRoleAdmin)
diff --git a/orchestra/apps/users/roles/jabber/models.py b/orchestra/apps/users/roles/jabber/models.py
new file mode 100644
index 00000000..32850579
--- /dev/null
+++ b/orchestra/apps/users/roles/jabber/models.py
@@ -0,0 +1,15 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from .. import roles
+
+
+class Jabber(models.Model):
+ user = models.OneToOneField('users.User', verbose_name=_("user"),
+ related_name='jabber')
+
+ def __unicode__(self):
+ return str(self.user)
+
+
+roles.register('jabber', Jabber)
diff --git a/orchestra/apps/users/roles/mail/__init__.py b/orchestra/apps/users/roles/mail/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/users/roles/mail/admin.py b/orchestra/apps/users/roles/mail/admin.py
new file mode 100644
index 00000000..58d3fa56
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/admin.py
@@ -0,0 +1,124 @@
+from django import forms
+from django.contrib import admin
+from django.contrib.auth import get_user_model
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.admin import ExtendedModelAdmin
+from orchestra.admin.utils import insertattr, admin_link
+from orchestra.apps.accounts.admin import SelectAccountAdminMixin
+from orchestra.apps.users.roles.admin import RoleAdmin
+
+from .forms import MailRoleAdminForm
+from .models import Mailbox, Address, Autoresponse
+
+
+class AutoresponseInline(admin.StackedInline):
+ model = Autoresponse
+ verbose_name_plural = _("autoresponse")
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name == 'subject':
+ kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
+ return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs)
+
+
+#class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
+# list_display = ('email', 'domain_link', 'mailboxes', 'forwards', 'account_link')
+# fields = ('account_link', ('name', 'domain'), 'destination')
+# inlines = [AutoresponseInline]
+# search_fields = ('name', 'domain__name',)
+# readonly_fields = ('account_link', 'domain_link', 'email_link')
+# filter_by_account_fields = ['domain']
+#
+# domain_link = link('domain', order='domain__name')
+#
+# def email_link(self, address):
+# link = self.domain_link(address)
+# return "%s@%s" % (address.name, link)
+# email_link.short_description = _("Email")
+# email_link.allow_tags = True
+#
+# def mailboxes(self, address):
+# boxes = []
+# for mailbox in address.get_mailboxes():
+# user = mailbox.user
+# url = reverse('admin:users_user_mailbox_change', args=(user.pk,))
+# boxes.append('%s' % (url, user.username))
+# return '
'.join(boxes)
+# mailboxes.allow_tags = True
+#
+# def forwards(self, address):
+# values = [ dest for dest in address.destination.split() if '@' in dest ]
+# return '
'.join(values)
+# forwards.allow_tags = True
+#
+# def formfield_for_dbfield(self, db_field, **kwargs):
+# if db_field.name == 'destination':
+# kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
+# return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+#
+# def queryset(self, request):
+# """ Select related for performance """
+# qs = super(AddressAdmin, self).queryset(request)
+# return qs.select_related('domain')
+
+
+class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
+ list_display = (
+ 'email', 'domain_link', 'display_mailboxes', 'display_forward', 'account_link'
+ )
+ fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward')
+ inlines = [AutoresponseInline]
+ search_fields = ('name', 'domain__name',)
+ readonly_fields = ('account_link', 'domain_link', 'email_link')
+ filter_by_account_fields = ['domain']
+ filter_horizontal = ['mailboxes']
+
+ domain_link = admin_link('domain', order='domain__name')
+
+ def email_link(self, address):
+ link = self.domain_link(address)
+ return "%s@%s" % (address.name, link)
+ email_link.short_description = _("Email")
+ email_link.allow_tags = True
+
+ def display_mailboxes(self, address):
+ boxes = []
+ for mailbox in address.mailboxes.all():
+ user = mailbox.user
+ url = reverse('admin:users_user_mailbox_change', args=(user.pk,))
+ boxes.append('%s' % (url, user.username))
+ return '
'.join(boxes)
+ display_mailboxes.short_description = _("Mailboxes")
+ display_mailboxes.allow_tags = True
+
+ def display_forward(self, address):
+ values = [ dest for dest in address.forward.split() ]
+ return '
'.join(values)
+ display_forward.short_description = _("Forward")
+ display_forward.allow_tags = True
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name == 'forward':
+ kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
+ if db_field.name == 'mailboxes':
+ mailboxes = db_field.rel.to.objects.select_related('user')
+ kwargs['queryset'] = mailboxes.filter(user__account=self.account)
+ return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+
+ def get_queryset(self, request):
+ """ Select related for performance """
+ qs = super(AddressAdmin, self).get_queryset(request)
+ return qs.select_related('domain')
+
+
+class MailRoleAdmin(RoleAdmin):
+ model = Mailbox
+ name = 'mailbox'
+ url_name = 'mailbox'
+ form = MailRoleAdminForm
+
+
+admin.site.register(Address, AddressAdmin)
+insertattr(get_user_model(), 'roles', MailRoleAdmin)
diff --git a/orchestra/apps/users/roles/mail/api.py b/orchestra/apps/users/roles/mail/api.py
new file mode 100644
index 00000000..410d8a1c
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/api.py
@@ -0,0 +1,27 @@
+from rest_framework import viewsets
+
+from orchestra.api import router
+from orchestra.apps.accounts.api import AccountApiMixin
+
+from .models import Address, Mailbox
+from .serializers import AddressSerializer, MailboxSerializer
+
+
+class AddressViewSet(AccountApiMixin, viewsets.ModelViewSet):
+ model = Address
+ serializer_class = AddressSerializer
+
+
+
+class MailboxViewSet(viewsets.ModelViewSet):
+ model = Mailbox
+ serializer_class = MailboxSerializer
+
+ def get_queryset(self):
+ qs = super(MailboxViewSet, self).get_queryset()
+ qs = qs.select_related('user')
+ return qs.filter(user__account=self.request.user.account_id)
+
+
+router.register(r'mailboxes', MailboxViewSet)
+router.register(r'addresses', AddressViewSet)
diff --git a/orchestra/apps/users/roles/mail/backends.py b/orchestra/apps/users/roles/mail/backends.py
new file mode 100644
index 00000000..b29d03ae
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/backends.py
@@ -0,0 +1,160 @@
+import os
+
+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
+
+
+class MailSystemUserBackend(ServiceController):
+ verbose_name = _("Mail system user")
+ model = 'mail.Mailbox'
+ # TODO related_models = ('resources__content_type') ??
+
+ DEFAULT_GROUP = 'postfix'
+
+ def create_user(self, context):
+ self.append(
+ "if [[ $( id %(username)s ) ]]; then \n"
+ " usermod -p '%(password)s' %(username)s \n"
+ "else \n"
+ " useradd %(username)s --password '%(password)s' \\\n"
+ " --shell /dev/null \n"
+ "fi" % context
+ )
+ self.append("mkdir -p %(home)s" % context)
+ self.append("chown %(username)s.%(group)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.use_custom_filtering:
+ context['filtering'] += mailbox.custom_filtering
+ else:
+ context['filtering'] += settings.EMAILS_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.create_user(context)
+ self.generate_filter(mailbox, context)
+
+ def delete(self, mailbox):
+ context = self.get_context(mailbox)
+ self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context)
+ self.append("killall -u %(username)s" % context)
+ self.append("userdel %(username)s" % context)
+ self.append("rm -fr %(home)s" % context)
+
+ def get_context(self, mailbox):
+ user = mailbox.user
+ context = {
+ 'username': user.username,
+ 'password': user.password if user.is_active else '*%s' % user.password,
+ 'group': self.DEFAULT_GROUP
+ }
+ context['home'] = settings.EMAILS_HOME % context
+ return context
+
+
+class PostfixAddressBackend(ServiceController):
+ verbose_name = _("Postfix address")
+ model = 'mail.Address'
+
+ def include_virtdomain(self, context):
+ self.append(
+ '[[ $(grep "^\s*%(domain)s\s*$" %(virtdomains)s) ]]'
+ ' || { echo "%(domain)s" >> %(virtdomains)s; UPDATED=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(
+ 'LINE="%(email)s\t%(destination)s"\n'
+ 'if [[ ! $(grep "^%(email)s\s" %(virtusertable)s) ]]; then\n'
+ ' echo "$LINE" >> %(virtusertable)s\n'
+ ' UPDATED=1\n'
+ 'else\n'
+ ' if [[ ! $(grep "^${LINE}$" %(virtusertable)s) ]]; then\n'
+ ' sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtusertable)s\n'
+ ' UPDATED=1\n'
+ ' fi\n'
+ 'fi' % context
+ )
+
+ def exclude_virtusertable(self, context):
+ self.append(
+ 'if [[ $(grep "^%(email)s\s") ]]; then\n'
+ ' sed -i "s/^%(email)s\s.*$//" %(virtusertable)s\n'
+ ' UPDATED=1\n'
+ '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('[[ $UPDATED == 1 ]] && { '
+ 'postmap %(virtdomains)s;'
+ 'postmap %(virtusertable)s;'
+ '}' % context)
+
+ def get_context_files(self):
+ return {
+ 'virtdomains': settings.EMAILS_VIRTDOMAINS_PATH,
+ 'virtusertable': settings.EMAILS_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 = 'email.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['home'] = settings.EMAILS_HOME % context
+ context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
+ context['object_id'] = mailbox.pk
+ return context
diff --git a/orchestra/apps/users/roles/mail/forms.py b/orchestra/apps/users/roles/mail/forms.py
new file mode 100644
index 00000000..7066e5d0
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/forms.py
@@ -0,0 +1,53 @@
+from django import forms
+from django.core.urlresolvers import reverse
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.forms.widgets import ReadOnlyWidget
+
+from .models import Mailbox
+from ..forms import RoleAdminBaseForm
+
+
+class MailRoleAdminForm(RoleAdminBaseForm):
+ class Meta(RoleAdminBaseForm.Meta):
+ model = Mailbox
+
+ def __init__(self, *args, **kwargs):
+ super(MailRoleAdminForm, self).__init__(*args, **kwargs)
+ instance = kwargs.get('instance')
+ if instance:
+ widget = ReadOnlyWidget(self.addresses(instance))
+ self.fields['addresses'] = forms.CharField(widget=widget,
+ label=_("Addresses"))
+
+# def addresses(self, mailbox):
+# account = mailbox.user.account
+# addresses = account.addresses.filter(destination__contains=mailbox.user.username)
+# add_url = reverse('admin:mail_address_add')
+# add_url += '?account=%d&destination=%s' % (account.pk, mailbox.user.username)
+# img = ''
+# onclick = 'onclick="return showAddAnotherPopup(this);"'
+# add_link = '%s Add address' % (add_url, onclick, img)
+# value = '%s
' % add_link
+# for pk, name, domain in addresses.values_list('pk', 'name', 'domain__name'):
+# url = reverse('admin:mail_address_change', args=(pk,))
+# name = '%s@%s' % (name, domain)
+# value += '
%s' % (url, name)
+# value = '' % value
+# return mark_safe('%s
' % value)
+
+ def addresses(self, mailbox):
+ account = mailbox.user.account
+ add_url = reverse('admin:mail_address_add')
+ add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk)
+ img = ''
+ onclick = 'onclick="return showAddAnotherPopup(this);"'
+ add_link = '%s Add address' % (add_url, onclick, img)
+ value = '%s
' % add_link
+ for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'):
+ url = reverse('admin:mail_address_change', args=(pk,))
+ name = '%s@%s' % (name, domain)
+ value += '%s' % (url, name)
+ value = '' % value
+ return mark_safe('%s
' % value)
diff --git a/orchestra/apps/users/roles/mail/models.py b/orchestra/apps/users/roles/mail/models.py
new file mode 100644
index 00000000..35d28355
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/models.py
@@ -0,0 +1,110 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.core import services
+
+from .. import roles
+
+from . import validators, settings
+
+
+class Mailbox(models.Model):
+ user = models.OneToOneField('users.User', verbose_name=_("User"),
+ related_name='mailbox')
+ use_custom_filtering = models.BooleanField(_("Use custom filtering"),
+ default=False)
+ custom_filtering = models.TextField(_("filtering"), blank=True,
+ validators=[validators.validate_sieve],
+ help_text=_("Arbitrary email filtering in sieve language."))
+
+ class Meta:
+ verbose_name_plural = _("mailboxes")
+
+ def __unicode__(self):
+ return self.user.username
+
+# def get_addresses(self):
+# regex = r'(^|\s)+%s(\s|$)+' % self.user.username
+# return Address.objects.filter(destination__regex=regex)
+#
+# def delete(self, *args, **kwargs):
+# """ Update related addresses """
+# regex = re.compile(r'(^|\s)+(\s*%s)(\s|$)+' % self.user.username)
+# super(Mailbox, self).delete(*args, **kwargs)
+# for address in self.get_addresses():
+# address.destination = regex.sub(r'\3', address.destination).strip()
+# if not address.destination:
+# address.delete()
+# else:
+# address.save()
+
+
+#class Address(models.Model):
+# name = models.CharField(_("name"), max_length=64,
+# validators=[validators.validate_emailname])
+# domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
+# verbose_name=_("domain"),
+# related_name='addresses')
+# destination = models.CharField(_("destination"), max_length=256,
+# validators=[validators.validate_destination],
+# help_text=_("Space separated mailbox names or email addresses"))
+# account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
+# related_name='addresses')
+#
+# class Meta:
+# verbose_name_plural = _("addresses")
+# unique_together = ('name', 'domain')
+#
+# def __unicode__(self):
+# return self.email
+#
+# @property
+# def email(self):
+# return "%s@%s" % (self.name, self.domain)
+#
+# def get_mailboxes(self):
+# for dest in self.destination.split():
+# if '@' not in dest:
+# yield Mailbox.objects.select_related('user').get(user__username=dest)
+
+
+class Address(models.Model):
+ name = models.CharField(_("name"), max_length=64,
+ validators=[validators.validate_emailname])
+ domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
+ verbose_name=_("domain"),
+ related_name='addresses')
+ mailboxes = models.ManyToManyField('mail.Mailbox',
+ verbose_name=_("mailboxes"),
+ related_name='addresses', blank=True)
+ forward = models.CharField(_("forward"), max_length=256, blank=True,
+ validators=[validators.validate_forward])
+ account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
+ related_name='addresses')
+
+ class Meta:
+ verbose_name_plural = _("addresses")
+ unique_together = ('name', 'domain')
+
+ def __unicode__(self):
+ return self.email
+
+ @property
+ def email(self):
+ return "%s@%s" % (self.name, self.domain)
+
+
+class Autoresponse(models.Model):
+ address = models.OneToOneField(Address, verbose_name=_("address"),
+ related_name='autoresponse')
+ # TODO initial_date
+ subject = models.CharField(_("subject"), max_length=256)
+ message = models.TextField(_("message"))
+ enabled = models.BooleanField(_("enabled"), default=False)
+
+ def __unicode__(self):
+ return self.address
+
+
+services.register(Address)
+roles.register('mailbox', Mailbox)
diff --git a/orchestra/apps/users/roles/mail/serializers.py b/orchestra/apps/users/roles/mail/serializers.py
new file mode 100644
index 00000000..24bf1a70
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/serializers.py
@@ -0,0 +1,43 @@
+from rest_framework import serializers
+
+from orchestra.api import router
+from orchestra.apps.accounts.serializers import AccountSerializerMixin
+
+from .models import Address, Mailbox
+
+
+#class AddressSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = Address
+# fields = ('url', 'name', 'domain', 'destination')
+
+
+class NestedMailboxSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Mailbox
+ fields = ('url', 'use_custom_filtering', 'custom_filtering')
+
+
+class MailboxSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Mailbox
+ fields = ('url', 'user', 'use_custom_filtering', 'custom_filtering')
+
+
+class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Address
+ fields = ('url', 'name', 'domain', 'mailboxes', 'forward')
+
+ def get_fields(self, *args, **kwargs):
+ fields = super(AddressSerializer, self).get_fields(*args, **kwargs)
+ account = self.context['view'].request.user.account_id
+ mailboxes = fields['mailboxes'].queryset.select_related('user')
+ fields['mailboxes'].queryset = mailboxes.filter(user__account=account)
+ # TODO do it on permissions or in self.filter_by_account_field ?
+ domain = fields['domain'].queryset
+ fields['domain'].queryset = domain .filter(account=account)
+ return fields
+
+
+router.insert('users', 'mailbox', NestedMailboxSerializer, required=False)
diff --git a/orchestra/apps/users/roles/mail/settings.py b/orchestra/apps/users/roles/mail/settings.py
new file mode 100644
index 00000000..fcca1e32
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/settings.py
@@ -0,0 +1,29 @@
+from django.conf import settings
+
+
+EMAILS_DOMAIN_MODEL = getattr(settings, 'EMAILS_DOMAIN_MODEL', 'domains.Domain')
+
+EMAILS_HOME = getattr(settings, 'EMAILS_HOME', '/home/%(username)s/')
+
+EMAILS_SIEVETEST_PATH = getattr(settings, 'EMAILS_SIEVETEST_PATH', '/dev/shm')
+
+EMAILS_SIEVETEST_BIN_PATH = getattr(settings, 'EMAILS_SIEVETEST_BIN_PATH',
+ '%(orchestra_root)s/bin/sieve-test')
+
+
+EMAILS_VIRTUSERTABLE_PATH = getattr(settings, 'EMAILS_VIRTUSERTABLE_PATH',
+ '/etc/postfix/virtusertable')
+
+
+EMAILS_VIRTDOMAINS_PATH = getattr(settings, 'EMAILS_VIRTDOMAINS_PATH',
+ '/etc/postfix/virtdomains')
+
+
+EMAILS_DEFAUL_FILTERING = getattr(settings, 'EMAILS_DEFAULT_FILTERING',
+ 'require ["fileinto","regex","envelope","vacation","reject","relational","comparator-i;ascii-numeric"];\n'
+ '\n'
+ 'if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-Score" "5" {\n'
+ ' fileinto "Junk";\n'
+ ' discard;\n'
+ '}'
+)
diff --git a/orchestra/apps/users/roles/mail/validators.py b/orchestra/apps/users/roles/mail/validators.py
new file mode 100644
index 00000000..eab400fa
--- /dev/null
+++ b/orchestra/apps/users/roles/mail/validators.py
@@ -0,0 +1,62 @@
+import hashlib
+import os
+import re
+
+from django.core.validators import ValidationError, EmailValidator
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.utils import paths
+from orchestra.utils.system import run
+
+from . import settings
+
+
+def validate_emailname(value):
+ msg = _("'%s' is not a correct email name" % value)
+ if '@' in value:
+ raise ValidationError(msg)
+ value += '@localhost'
+ try:
+ EmailValidator(value)
+ except ValidationError:
+ raise ValidationError(msg)
+
+
+#def validate_destination(value):
+# """ space separated mailboxes or emails """
+# for destination in value.split():
+# msg = _("'%s' is not an existent mailbox" % destination)
+# if '@' in destination:
+# if not destination[-1].isalpha():
+# raise ValidationError(msg)
+# EmailValidator(destination)
+# else:
+# from .models import Mailbox
+# if not Mailbox.objects.filter(user__username=destination).exists():
+# raise ValidationError(msg)
+# validate_emailname(destination)
+
+
+def validate_forward(value):
+ """ space separated mailboxes or emails """
+ for destination in value.split():
+ EmailValidator(destination)
+
+
+def validate_sieve(value):
+ sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
+ path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
+ with open(path, 'wb') as f:
+ f.write(value)
+ context = {
+ 'orchestra_root': paths.get_orchestra_root()
+ }
+ sievetest = settings.EMAILS_SIEVETEST_BIN_PATH % context
+ test = run(' '.join([sievetest, path, '/dev/null']), display=False)
+ if test.return_code:
+ errors = []
+ for line in test.stderr.splitlines():
+ error = re.match(r'^.*(line\s+[0-9]+:.*)', line)
+ if error:
+ errors += error.groups()
+ raise ValidationError(' '.join(errors))
diff --git a/orchestra/apps/users/roles/owncloud/__init__.py b/orchestra/apps/users/roles/owncloud/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/users/roles/posix/__init__.py b/orchestra/apps/users/roles/posix/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/orchestra/apps/users/roles/posix/admin.py b/orchestra/apps/users/roles/posix/admin.py
new file mode 100644
index 00000000..0ef5af77
--- /dev/null
+++ b/orchestra/apps/users/roles/posix/admin.py
@@ -0,0 +1,15 @@
+from django.contrib.auth import get_user_model
+
+from orchestra.admin.utils import insertattr
+from orchestra.apps.users.roles.admin import RoleAdmin
+
+from .models import POSIX
+
+
+class POSIXRoleAdmin(RoleAdmin):
+ model = POSIX
+ name = 'posix'
+ url_name = 'posix'
+
+
+insertattr(get_user_model(), 'roles', POSIXRoleAdmin)
diff --git a/orchestra/apps/users/roles/posix/models.py b/orchestra/apps/users/roles/posix/models.py
new file mode 100644
index 00000000..3639afc9
--- /dev/null
+++ b/orchestra/apps/users/roles/posix/models.py
@@ -0,0 +1,22 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from .. import roles
+
+from . import settings
+
+
+class POSIX(models.Model):
+ user = models.OneToOneField('users.User', verbose_name=_("user"),
+ related_name='posix')
+ home = models.CharField(_("home"), max_length=256, blank=True,
+ help_text=_("Home directory relative to account's ~primary_user"))
+ shell = models.CharField(_("shell"), max_length=32,
+ choices=settings.POSIX_SHELLS, default=settings.POSIX_DEFAULT_SHELL)
+
+ def __unicode__(self):
+ return str(self.user)
+
+# TODO groups
+
+roles.register('posix', POSIX)
diff --git a/orchestra/apps/users/roles/posix/serializers.py b/orchestra/apps/users/roles/posix/serializers.py
new file mode 100644
index 00000000..3dc341c5
--- /dev/null
+++ b/orchestra/apps/users/roles/posix/serializers.py
@@ -0,0 +1,14 @@
+from rest_framework import serializers
+
+from orchestra.api import router
+
+from .models import POSIX
+
+
+class POSIXSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = POSIX
+ fields = ('home', 'shell')
+
+
+router.insert('users', 'posix', POSIXSerializer, required=False)
diff --git a/orchestra/apps/users/roles/posix/settings.py b/orchestra/apps/users/roles/posix/settings.py
new file mode 100644
index 00000000..36860863
--- /dev/null
+++ b/orchestra/apps/users/roles/posix/settings.py
@@ -0,0 +1,11 @@
+from django.conf import settings
+from django.utils.translation import ugettext, ugettext_lazy as _
+
+
+POSIX_SHELLS = getattr(settings, 'POSIX_SHELLS', (
+ ('/bin/false', _("FTP/sFTP only")),
+ ('/bin/rsync', _("rsync shell")),
+ ('/bin/bash', "Bash"),
+))
+
+POSIX_DEFAULT_SHELL = getattr(settings, 'POSIX_DEFAULT_SHELL', '/bin/false')
diff --git a/orchestra/apps/users/templates/admin/users/user/change_form.html b/orchestra/apps/users/templates/admin/users/user/change_form.html
new file mode 100644
index 00000000..82814f98
--- /dev/null
+++ b/orchestra/apps/users/templates/admin/users/user/change_form.html
@@ -0,0 +1,15 @@
+{% extends "admin/change_form.html" %}
+{% load i18n admin_urls %}
+
+{% block object-tools-items %}
+ {% trans "User" %}
+{% for item in roles %}
+ {% if item.exists %}{{ item.name.capitalize }}{% else %}Add {{ item.name }}{% endif %}
+{% endfor %}
+
+ {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
+ {% trans "History" %}
+
+{% if has_absolute_url %}{% trans "View on site" %}{% endif%}
+{% endblock %}
+
diff --git a/orchestra/apps/users/templates/admin/users/user/delete_role.html b/orchestra/apps/users/templates/admin/users/user/delete_role.html
new file mode 100644
index 00000000..ae94df22
--- /dev/null
+++ b/orchestra/apps/users/templates/admin/users/user/delete_role.html
@@ -0,0 +1,15 @@
+{% extends "admin/delete_confirmation.html" %}
+{% load i18n admin_urls %}
+
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
diff --git a/orchestra/apps/users/templates/admin/users/user/role.html b/orchestra/apps/users/templates/admin/users/user/role.html
new file mode 100644
index 00000000..75927310
--- /dev/null
+++ b/orchestra/apps/users/templates/admin/users/user/role.html
@@ -0,0 +1,70 @@
+{% extends "admin/base_site.html" %}
+{% load i18n admin_urls admin_static admin_modify utils %}
+
+
+{% block extrastyle %}
+{{ block.super }}
+
+{{ media }}
+{% endblock %}
+
+{% block coltype %}colM{% endblock %}
+{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %}
+
+
+{% block breadcrumbs %}
+
+{% endblock %}
+
+
+
+{% block content %}
+{% block object-tools %}
+
+{% endblock %}
+
+
+
+{% endblock %}