Split email into a separated mails app
This commit is contained in:
parent
3ea6fde7bd
commit
ef7f3219a5
|
@ -1,7 +1,7 @@
|
||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
|
|
||||||
### 1.0a1 Milestone (first alpha release on May '14)
|
### 1.0a1 Milestone (first alpha release on Sep '14)
|
||||||
|
|
||||||
1. [x] Automated deployment of the development environment
|
1. [x] Automated deployment of the development environment
|
||||||
2. [x] Automated installation and upgrading
|
2. [x] Automated installation and upgrading
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
1. [ ] Initial documentation
|
1. [ ] Initial documentation
|
||||||
|
|
||||||
|
|
||||||
### 1.0b1 Milestone (first beta release on Jul '14)
|
### 1.0b1 Milestone (first beta release on Nov '14)
|
||||||
|
|
||||||
1. [x] Resource monitoring
|
1. [x] Resource monitoring
|
||||||
1. [ ] Orders
|
1. [ ] Orders
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
1. [ ] Full documentation
|
1. [ ] Full documentation
|
||||||
|
|
||||||
|
|
||||||
### 1.0 Milestone (first stable release on Dec '14)
|
### 1.0 Milestone (first stable release on Feb '15)
|
||||||
|
|
||||||
1. [ ] Stabilize data model, internal APIs and REST API
|
1. [ ] Stabilize data model, internal APIs and REST API
|
||||||
1. [ ] Integration with third-party service providers, e.g. Gandi
|
1. [ ] Integration with third-party service providers, e.g. Gandi
|
||||||
|
|
3
TODO.md
3
TODO.md
|
@ -73,3 +73,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
|
||||||
* backend logs with hal logo
|
* backend logs with hal logo
|
||||||
* Use logs for storing monitored values
|
* Use logs for storing monitored values
|
||||||
* set_password orchestration method?
|
* set_password orchestration method?
|
||||||
|
|
||||||
|
|
||||||
|
* make account_link to autoreplace account on change view.
|
||||||
|
|
|
@ -15,7 +15,8 @@ class Database(models.Model):
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=128,
|
name = models.CharField(_("name"), max_length=128,
|
||||||
validators=[validators.validate_name])
|
validators=[validators.validate_name])
|
||||||
users = models.ManyToManyField('databases.DatabaseUser', verbose_name=_("users"),
|
users = models.ManyToManyField('databases.DatabaseUser',
|
||||||
|
verbose_name=_("users"),
|
||||||
through='databases.Role', related_name='users')
|
through='databases.Role', related_name='users')
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
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, AccountAdminMixin
|
||||||
|
from orchestra.apps.domains.forms import DomainIterator
|
||||||
|
|
||||||
|
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
|
||||||
|
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 MailboxAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'name', 'account_link', 'use_custom_filtering', 'display_addresses'
|
||||||
|
)
|
||||||
|
list_filter = ('use_custom_filtering', HasAddressListFilter)
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('account', 'name'),
|
||||||
|
}),
|
||||||
|
(_("Filtering"), {
|
||||||
|
'fields': ('use_custom_filtering', 'custom_filtering'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('account_link', 'name'),
|
||||||
|
}),
|
||||||
|
(_("Filtering"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('use_custom_filtering', 'custom_filtering'),
|
||||||
|
}),
|
||||||
|
(_("Addresses"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('addresses_field',)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
readonly_fields = ('account_link', 'display_addresses', 'addresses_field')
|
||||||
|
|
||||||
|
def display_addresses(self, mailbox):
|
||||||
|
addresses = []
|
||||||
|
for addr in mailbox.addresses.all():
|
||||||
|
url = reverse('admin:mails_address_change', args=(addr.pk,))
|
||||||
|
addresses.append('<a href="%s">%s</a>' % (url, addr.email))
|
||||||
|
return '<br>'.join(addresses)
|
||||||
|
display_addresses.short_description = _("Addresses")
|
||||||
|
display_addresses.allow_tags = True
|
||||||
|
|
||||||
|
def addresses_field(self, mailbox):
|
||||||
|
""" Address form field with "Add address" button """
|
||||||
|
account = mailbox.account
|
||||||
|
add_url = reverse('admin:mails_address_add')
|
||||||
|
add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk)
|
||||||
|
img = '<img src="/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another">'
|
||||||
|
onclick = 'onclick="return showAddAnotherPopup(this);"'
|
||||||
|
add_link = '<a href="%s" %s>%s Add address</a>' % (add_url, onclick, img)
|
||||||
|
value = '%s<br><br>' % add_link
|
||||||
|
for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'):
|
||||||
|
url = reverse('admin:mails_address_change', args=(pk,))
|
||||||
|
name = '%s@%s' % (name, domain)
|
||||||
|
value += '<li><a href="%s">%s</a></li>' % (url, name)
|
||||||
|
value = '<ul>%s</ul>' % value
|
||||||
|
return mark_safe('<div style="padding-left: 100px;">%s</div>' % value)
|
||||||
|
addresses_field.short_description = _("Addresses")
|
||||||
|
addresses_field.allow_tags = True
|
||||||
|
|
||||||
|
|
||||||
|
class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'email', 'domain_link', 'display_mailboxes', 'display_forward', 'account_link'
|
||||||
|
)
|
||||||
|
list_filter = (HasMailboxListFilter, HasForwardListFilter)
|
||||||
|
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', 'mailboxes')
|
||||||
|
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():
|
||||||
|
url = reverse('admin:mails_mailbox_change', args=(mailbox.pk,))
|
||||||
|
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
||||||
|
return '<br>'.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 '<br>'.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'})
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Mailbox, MailboxAdmin)
|
||||||
|
admin.site.register(Address, AddressAdmin)
|
|
@ -0,0 +1,22 @@
|
||||||
|
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(AccountApiMixin, viewsets.ModelViewSet):
|
||||||
|
model = Mailbox
|
||||||
|
serializer_class = MailboxSerializer
|
||||||
|
|
||||||
|
|
||||||
|
router.register(r'mailboxes', MailboxViewSet)
|
||||||
|
router.register(r'addresses', AddressViewSet)
|
|
@ -0,0 +1,159 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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(site)
|
||||||
|
context['home'] = settings.EMAILS_HOME % context
|
||||||
|
context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
|
||||||
|
context['object_id'] = mailbox.pk
|
||||||
|
return context
|
|
@ -0,0 +1,48 @@
|
||||||
|
from django.contrib.admin import SimpleListFilter
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class HasMailboxListFilter(SimpleListFilter):
|
||||||
|
""" Filter addresses whether they have any mailbox or not """
|
||||||
|
title = _("Has mailbox")
|
||||||
|
parameter_name = 'has_mailbox'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
('True', _("True")),
|
||||||
|
('False', _("False")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'True':
|
||||||
|
return queryset.filter(mailboxes__isnull=False)
|
||||||
|
elif self.value() == 'False':
|
||||||
|
return queryset.filter(mailboxes__isnull=True)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class HasForwardListFilter(HasMailboxListFilter):
|
||||||
|
""" Filter addresses whether they have any mailbox or not """
|
||||||
|
title = _("Has forward")
|
||||||
|
parameter_name = 'has_forward'
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'True':
|
||||||
|
return queryset.exclude(forward='')
|
||||||
|
elif self.value() == 'False':
|
||||||
|
return queryset.filter(forward='')
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class HasAddressListFilter(HasMailboxListFilter):
|
||||||
|
""" Filter addresses whether they have any mailbox or not """
|
||||||
|
title = _("Has address")
|
||||||
|
parameter_name = 'has_address'
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'True':
|
||||||
|
return queryset.filter(addresses__isnull=False)
|
||||||
|
elif self.value() == 'False':
|
||||||
|
return queryset.filter(addresses__isnull=True)
|
||||||
|
return queryset
|
||||||
|
|
|
@ -15,62 +15,23 @@ class Mailbox(models.Model):
|
||||||
help_text=_("Required. 30 characters or fewer. Letters, digits and "
|
help_text=_("Required. 30 characters or fewer. Letters, digits and "
|
||||||
"@/./+/-/_ only."),
|
"@/./+/-/_ only."),
|
||||||
validators=[RegexValidator(r'^[\w.@+-]+$',
|
validators=[RegexValidator(r'^[\w.@+-]+$',
|
||||||
_("Enter a valid username."), 'invalid')])
|
_("Enter a valid mailbox name."), 'invalid')])
|
||||||
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
|
related_name='mailboxes')
|
||||||
use_custom_filtering = models.BooleanField(_("Use custom filtering"),
|
use_custom_filtering = models.BooleanField(_("Use custom filtering"),
|
||||||
default=False)
|
default=False)
|
||||||
custom_filtering = models.TextField(_("filtering"), blank=True,
|
custom_filtering = models.TextField(_("filtering"), blank=True,
|
||||||
validators=[validators.validate_sieve],
|
validators=[validators.validate_sieve],
|
||||||
help_text=_("Arbitrary email filtering in sieve language."))
|
help_text=_("Arbitrary email filtering in sieve language."))
|
||||||
|
# addresses = models.ManyToManyField('mails.Address',
|
||||||
|
# verbose_name=_("addresses"),
|
||||||
|
# related_name='mailboxes', blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = _("mailboxes")
|
verbose_name_plural = _("mailboxes")
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.user.username
|
return self.name
|
||||||
|
|
||||||
# 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):
|
class Address(models.Model):
|
||||||
|
@ -79,7 +40,8 @@ class Address(models.Model):
|
||||||
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
|
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
|
||||||
verbose_name=_("domain"),
|
verbose_name=_("domain"),
|
||||||
related_name='addresses')
|
related_name='addresses')
|
||||||
mailboxes = models.ManyToManyField('mail.Mailbox', verbose_name=_("mailboxes"),
|
mailboxes = models.ManyToManyField(Mailbox,
|
||||||
|
verbose_name=_("mailboxes"),
|
||||||
related_name='addresses', blank=True)
|
related_name='addresses', blank=True)
|
||||||
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
||||||
validators=[validators.validate_forward])
|
validators=[validators.validate_forward])
|
||||||
|
@ -110,4 +72,5 @@ class Autoresponse(models.Model):
|
||||||
return self.address
|
return self.address
|
||||||
|
|
||||||
|
|
||||||
|
services.register(Mailbox)
|
||||||
services.register(Address)
|
services.register(Address)
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||||
|
|
||||||
|
from .models import Mailbox, Address
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Mailbox
|
||||||
|
fields = ('url', 'name', 'use_custom_filtering', 'custom_filtering', 'addresses')
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
fields['mailboxes'].queryset = mailboxes.filter(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
|
|
@ -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', '/var/vmail/%(account)s/%(name)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'
|
||||||
|
'}'
|
||||||
|
)
|
|
@ -0,0 +1,63 @@
|
||||||
|
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):
|
||||||
|
from .models import Mailbox
|
||||||
|
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))
|
|
@ -92,6 +92,11 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
|
||||||
display_registered_on = admin_date('registered_on')
|
display_registered_on = admin_date('registered_on')
|
||||||
display_cancelled_on = admin_date('cancelled_on')
|
display_cancelled_on = admin_date('cancelled_on')
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
qs = super(OrderAdmin, self).get_queryset(request)
|
||||||
|
return qs.select_related('service').prefetch_related('content_object')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MetricStorageAdmin(admin.ModelAdmin):
|
class MetricStorageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('order', 'value', 'created_on', 'updated_on')
|
list_display = ('order', 'value', 'created_on', 'updated_on')
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
class ActiveOrderListFilter(SimpleListFilter):
|
class ActiveOrderListFilter(SimpleListFilter):
|
||||||
""" Filter tickets by created_by according to request.user """
|
""" Filter tickets by created_by according to request.user """
|
||||||
title = 'Orders'
|
title = _("Orders")
|
||||||
parameter_name = 'is_active'
|
parameter_name = 'is_active'
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
|
|
|
@ -45,6 +45,7 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
|
||||||
form = UserChangeForm
|
form = UserChangeForm
|
||||||
roles = []
|
roles = []
|
||||||
ordering = ('-id',)
|
ordering = ('-id',)
|
||||||
|
change_form_template = 'admin/users/user/change_form.html'
|
||||||
|
|
||||||
def display_is_main(self, instance):
|
def display_is_main(self, instance):
|
||||||
return instance.is_main
|
return instance.is_main
|
||||||
|
|
|
@ -10,8 +10,8 @@ from orchestra.core import services
|
||||||
class User(auth.AbstractBaseUser):
|
class User(auth.AbstractBaseUser):
|
||||||
username = models.CharField(_("username"), max_length=64, unique=True,
|
username = models.CharField(_("username"), max_length=64, unique=True,
|
||||||
help_text=_("Required. 30 characters or fewer. Letters, digits and "
|
help_text=_("Required. 30 characters or fewer. Letters, digits and "
|
||||||
"@/./+/-/_ only."),
|
"./-/_ only."),
|
||||||
validators=[validators.RegexValidator(r'^[\w.@+-]+$',
|
validators=[validators.RegexValidator(r'^[\w.-]+$',
|
||||||
_("Enter a valid username."), 'invalid')])
|
_("Enter a valid username."), 'invalid')])
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='users', null=True)
|
related_name='users', null=True)
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
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):
|
||||||
|
try:
|
||||||
|
getattr(user, name)
|
||||||
|
except models.DoesNotExist:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
setattr(User, 'has_%s' % name, has_role)
|
||||||
|
self._registry[name] = model
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return self._registry
|
||||||
|
|
||||||
|
|
||||||
|
roles = Register()
|
|
@ -1,6 +1,8 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .. import roles
|
||||||
|
|
||||||
|
|
||||||
class Jabber(models.Model):
|
class Jabber(models.Model):
|
||||||
user = models.OneToOneField('users.User', verbose_name=_("user"),
|
user = models.OneToOneField('users.User', verbose_name=_("user"),
|
||||||
|
@ -8,3 +10,6 @@ class Jabber(models.Model):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
|
|
||||||
|
|
||||||
|
roles.register('jabber', Jabber)
|
||||||
|
|
|
@ -7,6 +7,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
|
||||||
|
from .. import roles
|
||||||
|
|
||||||
from . import validators, settings
|
from . import validators, settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,7 +78,8 @@ class Address(models.Model):
|
||||||
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
|
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
|
||||||
verbose_name=_("domain"),
|
verbose_name=_("domain"),
|
||||||
related_name='addresses')
|
related_name='addresses')
|
||||||
mailboxes = models.ManyToManyField('mail.Mailbox', verbose_name=_("mailboxes"),
|
mailboxes = models.ManyToManyField('mail.Mailbox',
|
||||||
|
verbose_name=_("mailboxes"),
|
||||||
related_name='addresses', blank=True)
|
related_name='addresses', blank=True)
|
||||||
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
forward = models.CharField(_("forward"), max_length=256, blank=True,
|
||||||
validators=[validators.validate_forward])
|
validators=[validators.validate_forward])
|
||||||
|
@ -108,3 +111,4 @@ class Autoresponse(models.Model):
|
||||||
|
|
||||||
|
|
||||||
services.register(Address)
|
services.register(Address)
|
||||||
|
roles.register('mailbox', Mailbox)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .. import roles
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,3 +16,6 @@ class POSIX(models.Model):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
|
|
||||||
|
|
||||||
|
roles.register('posix', POSIX)
|
||||||
|
|
|
@ -68,9 +68,10 @@ INSTALLED_APPS = (
|
||||||
'orchestra.apps.orchestration',
|
'orchestra.apps.orchestration',
|
||||||
'orchestra.apps.domains',
|
'orchestra.apps.domains',
|
||||||
'orchestra.apps.users',
|
'orchestra.apps.users',
|
||||||
'orchestra.apps.users.roles.mail',
|
# 'orchestra.apps.users.roles.mail',
|
||||||
'orchestra.apps.users.roles.jabber',
|
'orchestra.apps.users.roles.jabber',
|
||||||
'orchestra.apps.users.roles.posix',
|
'orchestra.apps.users.roles.posix',
|
||||||
|
'orchestra.apps.mails',
|
||||||
'orchestra.apps.lists',
|
'orchestra.apps.lists',
|
||||||
'orchestra.apps.webapps',
|
'orchestra.apps.webapps',
|
||||||
'orchestra.apps.websites',
|
'orchestra.apps.websites',
|
||||||
|
@ -168,8 +169,9 @@ FLUENT_DASHBOARD_APP_GROUPS = (
|
||||||
FLUENT_DASHBOARD_APP_ICONS = {
|
FLUENT_DASHBOARD_APP_ICONS = {
|
||||||
# Services
|
# Services
|
||||||
'webs/web': 'web.png',
|
'webs/web': 'web.png',
|
||||||
'mail/mailbox': 'email.png',
|
|
||||||
'mail/address': 'X-office-address-book.png',
|
'mail/address': 'X-office-address-book.png',
|
||||||
|
'mails/mailbox': 'email.png',
|
||||||
|
'mails/address': 'X-office-address-book.png',
|
||||||
'lists/list': 'email-alter.png',
|
'lists/list': 'email-alter.png',
|
||||||
'domains/domain': 'domain.png',
|
'domains/domain': 'domain.png',
|
||||||
'multitenance/tenant': 'apps.png',
|
'multitenance/tenant': 'apps.png',
|
||||||
|
|
Loading…
Reference in New Issue