Added mailbox-address cross-validation
This commit is contained in:
parent
5291df3467
commit
b4dddef777
5
TODO.md
5
TODO.md
|
@ -187,8 +187,6 @@ https://code.djangoproject.com/ticket/24576
|
||||||
# FIXME what to do when deleting accounts? set fk null and fill a username charfield? issues, invoices.. we whant all this to go away?
|
# FIXME what to do when deleting accounts? set fk null and fill a username charfield? issues, invoices.. we whant all this to go away?
|
||||||
* implement delete All related services
|
* implement delete All related services
|
||||||
|
|
||||||
# FIXME address name change does not remove old one :P, readonly or perhaps we can regenerate all addresses using backend.prepare()?
|
|
||||||
|
|
||||||
* read https://docs.djangoproject.com/en/dev/releases/1.8/ and fix deprecation warnings
|
* read https://docs.djangoproject.com/en/dev/releases/1.8/ and fix deprecation warnings
|
||||||
|
|
||||||
* create nice fieldsets for SaaS, WebApp types and services, and helptexts too!
|
* create nice fieldsets for SaaS, WebApp types and services, and helptexts too!
|
||||||
|
@ -412,7 +410,6 @@ http://makandracards.com/makandra/24933-chrome-34+-firefox-38+-ie11+-ignore-auto
|
||||||
mkhomedir_helper or create ssh homes with bash.rc and such
|
mkhomedir_helper or create ssh homes with bash.rc and such
|
||||||
|
|
||||||
# warnings if some plugins are disabled, like make routes red
|
# warnings if some plugins are disabled, like make routes red
|
||||||
|
|
||||||
# replace show emails by https://docs.python.org/3/library/email.contentmanager.html#module-email.contentmanager
|
# replace show emails by https://docs.python.org/3/library/email.contentmanager.html#module-email.contentmanager
|
||||||
|
|
||||||
# SElect contact list breadcrumbs
|
# put addressform.clean on model.clean and search for other places?
|
||||||
|
|
|
@ -169,6 +169,7 @@ MIDDLEWARE_CLASSES = (
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
|
# 'django.middleware.locale.LocaleMiddleware'
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
|
|
@ -76,24 +76,22 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
||||||
return super(AccountAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(AccountAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
def change_view(self, request, object_id, form_url='', extra_context=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
if request.method == 'GET':
|
if not add:
|
||||||
account = self.get_object(request, unquote(object_id))
|
if request.method == 'GET' and not obj.is_active:
|
||||||
if not account.is_active:
|
|
||||||
messages.warning(request, 'This account is disabled.')
|
messages.warning(request, 'This account is disabled.')
|
||||||
context = {
|
context.update({
|
||||||
'services': sorted(
|
'services': sorted(
|
||||||
[model._meta for model in services.get() if model is not Account],
|
[model._meta for model in services.get() if model is not Account],
|
||||||
key=lambda i: i.verbose_name_plural.lower()
|
key=lambda i: i.verbose_name_plural.lower()
|
||||||
),
|
),
|
||||||
'accounts': sorted(
|
'accounts': sorted(
|
||||||
[model._meta for model in accounts.get() if model is not Account],
|
[model._meta for model in accounts.get() if model is not Account],
|
||||||
key=lambda i: i.verbose_name_plural.lower()
|
key=lambda i: i.verbose_name_plural.lower()
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
context.update(extra_context or {})
|
return super(AccountAdmin, self).render_change_form(
|
||||||
return super(AccountAdmin, self).change_view(
|
request, context, add, change, form_url, obj)
|
||||||
request, object_id, form_url=form_url, extra_context=context)
|
|
||||||
|
|
||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
|
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
|
||||||
|
|
|
@ -82,18 +82,15 @@ class LogEntryAdmin(admin.ModelAdmin):
|
||||||
content_object_link.admin_order_field = 'object_repr'
|
content_object_link.admin_order_field = 'object_repr'
|
||||||
content_object_link.allow_tags = True
|
content_object_link.allow_tags = True
|
||||||
|
|
||||||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||||
""" Add rel_opts and object to context """
|
""" Add rel_opts and object to context """
|
||||||
context = {}
|
if not add and 'edit' in request.GET.urlencode():
|
||||||
if 'edit' in request.GET.urlencode():
|
context.update({
|
||||||
obj = self.get_object(request, unquote(object_id))
|
|
||||||
context = {
|
|
||||||
'rel_opts': obj.content_type.model_class()._meta,
|
'rel_opts': obj.content_type.model_class()._meta,
|
||||||
'object': obj,
|
'object': obj,
|
||||||
}
|
})
|
||||||
context.update(extra_context or {})
|
return super(LogEntryAdmin, self).render_change_form(
|
||||||
return super(LogEntryAdmin, self).changeform_view(
|
request, context, add, change, form_url, obj)
|
||||||
request, object_id, form_url, extra_context=context)
|
|
||||||
|
|
||||||
def response_change(self, request, obj):
|
def response_change(self, request, obj):
|
||||||
""" save and continue preserve edit query string """
|
""" save and continue preserve edit query string """
|
||||||
|
|
|
@ -2,9 +2,11 @@ import copy
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin, messages
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.db.models import F, Value as V
|
from django.db.models import F, Value as V
|
||||||
from django.db.models.functions import Concat
|
from django.db.models.functions import Concat
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
|
@ -122,8 +124,27 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
search_term = search_term.replace('@', ' ')
|
search_term = search_term.replace('@', ' ')
|
||||||
return super(MailboxAdmin, self).get_search_results(request, queryset, search_term)
|
return super(MailboxAdmin, self).get_search_results(request, queryset, search_term)
|
||||||
|
|
||||||
|
def render_change_form(self, request, context, *args, **kwargs):
|
||||||
|
# Check if there exists an unrelated local Address for this mbox
|
||||||
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
|
obj = kwargs['obj']
|
||||||
|
if local_domain and obj.name:
|
||||||
|
non_mbox_addresses = Address.objects.exclude(mailboxes__name=obj.name).exclude(
|
||||||
|
forward__regex=r'.*(^|\s)+%s($|\s)+.*' % obj.name)
|
||||||
|
try:
|
||||||
|
addr = non_mbox_addresses.get(name=obj.name, domain__name=local_domain)
|
||||||
|
except Address.DoesNotExist:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
url = reverse('admin:mailboxes_address_change', args=(addr.pk,))
|
||||||
|
msg = _("Address <a href='{url}'>{addr}</a> clashes with this mailbox "
|
||||||
|
"local address. Consider adding this mailbox to the address.").format(
|
||||||
|
url=url, addr=addr)
|
||||||
|
self.message_user(request, mark_safe(msg), level=messages.WARNING)
|
||||||
|
return super(MailboxAdmin, self).render_change_form(request, context, *args, **kwargs)
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
""" save hacky mailbox.addresses """
|
""" save hacky mailbox.addresses and local domain clashing """
|
||||||
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
||||||
obj.addresses = form.cleaned_data['addresses']
|
obj.addresses = form.cleaned_data['addresses']
|
||||||
|
|
||||||
|
|
|
@ -11,3 +11,4 @@ class MailboxesConfig(AppConfig):
|
||||||
from .models import Mailbox, Address
|
from .models import Mailbox, Address
|
||||||
services.register(Mailbox, icon='email.png')
|
services.register(Mailbox, icon='email.png')
|
||||||
services.register(Address, icon='X-office-address-book.png')
|
services.register(Address, icon='X-office-address-book.png')
|
||||||
|
from . import signals
|
||||||
|
|
|
@ -10,7 +10,7 @@ from orchestra.contrib.orchestration import ServiceController
|
||||||
from orchestra.contrib.resources import ServiceMonitor
|
from orchestra.contrib.resources import ServiceMonitor
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from .models import Address
|
from .models import Address, Mailbox
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -137,97 +137,97 @@ class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class DovecotPostfixPasswdVirtualUserBackend(SieveFilteringMixin, ServiceController):
|
#class DovecotPostfixPasswdVirtualUserBackend(SieveFilteringMixin, ServiceController):
|
||||||
"""
|
# """
|
||||||
WARNING: This backends is not fully implemented
|
# WARNING: This backends is not fully implemented
|
||||||
"""
|
# """
|
||||||
DEFAULT_GROUP = 'postfix'
|
# DEFAULT_GROUP = 'postfix'
|
||||||
|
#
|
||||||
verbose_name = _("Dovecot-Postfix virtualuser")
|
# verbose_name = _("Dovecot-Postfix virtualuser")
|
||||||
model = 'mailboxes.Mailbox'
|
# model = 'mailboxes.Mailbox'
|
||||||
|
#
|
||||||
def set_user(self, context):
|
# def set_user(self, context):
|
||||||
self.append(textwrap.dedent("""
|
# self.append(textwrap.dedent("""
|
||||||
if grep '^%(user)s:' %(passwd_path)s > /dev/null ; then
|
# if grep '^%(user)s:' %(passwd_path)s > /dev/null ; then
|
||||||
sed -i 's#^%(user)s:.*#%(passwd)s#' %(passwd_path)s
|
# sed -i 's#^%(user)s:.*#%(passwd)s#' %(passwd_path)s
|
||||||
else
|
# else
|
||||||
echo '%(passwd)s' >> %(passwd_path)s
|
# echo '%(passwd)s' >> %(passwd_path)s
|
||||||
fi""") % context
|
# fi""") % context
|
||||||
)
|
# )
|
||||||
self.append("mkdir -p %(home)s" % context)
|
# self.append("mkdir -p %(home)s" % context)
|
||||||
self.append("chown %(uid)s:%(gid)s %(home)s" % context)
|
# self.append("chown %(uid)s:%(gid)s %(home)s" % context)
|
||||||
|
#
|
||||||
def set_mailbox(self, context):
|
# def set_mailbox(self, context):
|
||||||
self.append(textwrap.dedent("""
|
# self.append(textwrap.dedent("""
|
||||||
if ! grep '^%(user)s@%(mailbox_domain)s\s' %(virtual_mailbox_maps)s > /dev/null; then
|
# 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
|
# echo "%(user)s@%(mailbox_domain)s\tOK" >> %(virtual_mailbox_maps)s
|
||||||
UPDATED_VIRTUAL_MAILBOX_MAPS=1
|
# UPDATED_VIRTUAL_MAILBOX_MAPS=1
|
||||||
fi""") % context
|
# fi""") % context
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
def save(self, mailbox):
|
# def save(self, mailbox):
|
||||||
context = self.get_context(mailbox)
|
# context = self.get_context(mailbox)
|
||||||
self.set_user(context)
|
# self.set_user(context)
|
||||||
self.set_mailbox(context)
|
# self.set_mailbox(context)
|
||||||
self.generate_filter(mailbox, context)
|
# self.generate_filter(mailbox, context)
|
||||||
|
#
|
||||||
def delete(self, mailbox):
|
# def delete(self, mailbox):
|
||||||
context = self.get_context(mailbox)
|
# context = self.get_context(mailbox)
|
||||||
self.append(textwrap.dedent("""
|
# self.append(textwrap.dedent("""
|
||||||
nohup bash -c 'sleep 2 && killall -u %(uid)s -s KILL' &> /dev/null &
|
# nohup bash -c 'sleep 2 && killall -u %(uid)s -s KILL' &> /dev/null &
|
||||||
killall -u %(uid)s || true
|
# killall -u %(uid)s || true
|
||||||
sed -i '/^%(user)s:.*/d' %(passwd_path)s
|
# sed -i '/^%(user)s:.*/d' %(passwd_path)s
|
||||||
sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s
|
# sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s
|
||||||
UPDATED_VIRTUAL_MAILBOX_MAPS=1""") % context
|
# UPDATED_VIRTUAL_MAILBOX_MAPS=1""") % context
|
||||||
)
|
# )
|
||||||
if context['deleted_home']:
|
# if context['deleted_home']:
|
||||||
self.append("mv %(home)s %(deleted_home)s || exit_code=$?" % context)
|
# self.append("mv %(home)s %(deleted_home)s || exit_code=$?" % context)
|
||||||
else:
|
# else:
|
||||||
self.append("rm -fr %(home)s" % context)
|
# self.append("rm -fr %(home)s" % context)
|
||||||
|
#
|
||||||
def get_extra_fields(self, mailbox, context):
|
# def get_extra_fields(self, mailbox, context):
|
||||||
context['quota'] = self.get_quota(mailbox)
|
# context['quota'] = self.get_quota(mailbox)
|
||||||
return 'userdb_mail=maildir:~/Maildir {quota}'.format(**context)
|
# return 'userdb_mail=maildir:~/Maildir {quota}'.format(**context)
|
||||||
|
#
|
||||||
def get_quota(self, mailbox):
|
# def get_quota(self, mailbox):
|
||||||
try:
|
# try:
|
||||||
quota = mailbox.resources.disk.allocated
|
# quota = mailbox.resources.disk.allocated
|
||||||
except (AttributeError, ObjectDoesNotExist):
|
# except (AttributeError, ObjectDoesNotExist):
|
||||||
return ''
|
# return ''
|
||||||
unit = mailbox.resources.disk.unit[0].upper()
|
# unit = mailbox.resources.disk.unit[0].upper()
|
||||||
return 'userdb_quota_rule=*:bytes=%i%s' % (quota, unit)
|
# return 'userdb_quota_rule=*:bytes=%i%s' % (quota, unit)
|
||||||
|
#
|
||||||
def commit(self):
|
# def commit(self):
|
||||||
context = {
|
# context = {
|
||||||
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
|
# 'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
|
||||||
}
|
# }
|
||||||
self.append(textwrap.dedent("""
|
# self.append(textwrap.dedent("""
|
||||||
[[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && {
|
# [[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && {
|
||||||
postmap %(virtual_mailbox_maps)s
|
# postmap %(virtual_mailbox_maps)s
|
||||||
}""") % context
|
# }""") % context
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
def get_context(self, mailbox):
|
# def get_context(self, mailbox):
|
||||||
context = {
|
# context = {
|
||||||
'name': mailbox.name,
|
# 'name': mailbox.name,
|
||||||
'user': mailbox.name,
|
# 'user': mailbox.name,
|
||||||
'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
|
# 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
|
||||||
'uid': 10000 + mailbox.pk,
|
# 'uid': 10000 + mailbox.pk,
|
||||||
'gid': 10000 + mailbox.pk,
|
# 'gid': 10000 + mailbox.pk,
|
||||||
'group': self.DEFAULT_GROUP,
|
# 'group': self.DEFAULT_GROUP,
|
||||||
'quota': self.get_quota(mailbox),
|
# 'quota': self.get_quota(mailbox),
|
||||||
'passwd_path': settings.MAILBOXES_PASSWD_PATH,
|
# 'passwd_path': settings.MAILBOXES_PASSWD_PATH,
|
||||||
'home': mailbox.get_home(),
|
# 'home': mailbox.get_home(),
|
||||||
'banner': self.get_banner(),
|
# 'banner': self.get_banner(),
|
||||||
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH,
|
# 'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH,
|
||||||
'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
|
# 'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
|
||||||
}
|
# }
|
||||||
context['extra_fields'] = self.get_extra_fields(mailbox, context)
|
# context['extra_fields'] = self.get_extra_fields(mailbox, context)
|
||||||
context.update({
|
# context.update({
|
||||||
'passwd': '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context),
|
# 'passwd': '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context),
|
||||||
'deleted_home': settings.MAILBOXES_MOVE_ON_DELETE_PATH % context,
|
# 'deleted_home': settings.MAILBOXES_MOVE_ON_DELETE_PATH % context,
|
||||||
})
|
# })
|
||||||
return context
|
# return context
|
||||||
|
|
||||||
|
|
||||||
class PostfixAddressVirtualDomainBackend(ServiceController):
|
class PostfixAddressVirtualDomainBackend(ServiceController):
|
||||||
|
@ -301,6 +301,7 @@ class PostfixAddressVirtualDomainBackend(ServiceController):
|
||||||
def get_context(self, address):
|
def get_context(self, address):
|
||||||
context = self.get_context_files()
|
context = self.get_context_files()
|
||||||
context.update({
|
context.update({
|
||||||
|
'name': address.name,
|
||||||
'domain': address.domain,
|
'domain': address.domain,
|
||||||
'email': address.email,
|
'email': address.email,
|
||||||
'local_domain': settings.MAILBOXES_LOCAL_DOMAIN,
|
'local_domain': settings.MAILBOXES_LOCAL_DOMAIN,
|
||||||
|
@ -319,10 +320,19 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend):
|
||||||
'MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH'
|
'MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
def update_virtual_alias_maps(self, address, context):
|
def update_virtual_alias_maps(self, address, context):
|
||||||
destination = address.destination
|
context['destination'] = address.destination
|
||||||
if destination:
|
if not self.is_implicit_entry(context):
|
||||||
context['destination'] = destination
|
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""
|
||||||
# Set virtual alias entry for %(email)s
|
# Set virtual alias entry for %(email)s
|
||||||
LINE='%(email)s\t%(destination)s'
|
LINE='%(email)s\t%(destination)s'
|
||||||
|
@ -338,7 +348,12 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend):
|
||||||
fi
|
fi
|
||||||
fi""") % context)
|
fi""") % context)
|
||||||
else:
|
else:
|
||||||
logger.warning("Address %i is empty" % address.pk)
|
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)
|
self.exclude_virtual_alias_maps(context)
|
||||||
# Virtual mailbox stuff
|
# Virtual mailbox stuff
|
||||||
# destination = []
|
# destination = []
|
||||||
|
@ -350,7 +365,7 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend):
|
||||||
# destination.append(forward)
|
# destination.append(forward)
|
||||||
|
|
||||||
def exclude_virtual_alias_maps(self, context):
|
def exclude_virtual_alias_maps(self, context):
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""\
|
||||||
# Remove %(email)s virtual alias entry
|
# Remove %(email)s virtual alias entry
|
||||||
if grep '^%(email)s\s' %(virtual_alias_maps)s > /dev/null; then
|
if grep '^%(email)s\s' %(virtual_alias_maps)s > /dev/null; then
|
||||||
sed -i '/^%(email)s\s/d' %(virtual_alias_maps)s
|
sed -i '/^%(email)s\s/d' %(virtual_alias_maps)s
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.admin import widgets
|
from django.contrib.admin import widgets
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.forms import UserCreationForm, UserChangeForm
|
from orchestra.forms import UserCreationForm, UserChangeForm
|
||||||
from orchestra.utils.python import AttrDict
|
from orchestra.utils.python import AttrDict
|
||||||
|
|
||||||
from .models import Address
|
from . import settings
|
||||||
|
from .models import Address, Mailbox
|
||||||
|
|
||||||
|
|
||||||
class MailboxForm(forms.ModelForm):
|
class MailboxForm(forms.ModelForm):
|
||||||
|
@ -37,21 +39,28 @@ class MailboxForm(forms.ModelForm):
|
||||||
return mark_safe(output)
|
return mark_safe(output)
|
||||||
self.fields['addresses'].widget.render = render
|
self.fields['addresses'].widget.render = render
|
||||||
queryset = self.fields['addresses'].queryset
|
queryset = self.fields['addresses'].queryset
|
||||||
realted_addresses = queryset.filter(account=self.modeladmin.account.pk).order_by('name')
|
realted_addresses = queryset.filter(account_id=self.modeladmin.account.pk).order_by('name')
|
||||||
self.fields['addresses'].queryset = realted_addresses
|
self.fields['addresses'].queryset = realted_addresses
|
||||||
|
|
||||||
if self.instance and self.instance.pk:
|
if self.instance and self.instance.pk:
|
||||||
self.fields['addresses'].initial = self.instance.addresses.all()
|
self.fields['addresses'].initial = self.instance.addresses.all()
|
||||||
|
|
||||||
def clean_custom_filtering(self):
|
def clean(self):
|
||||||
# TODO move to model.clean?
|
cleaned_data = super(MailboxForm, self).clean()
|
||||||
filtering = self.cleaned_data['filtering']
|
name = self.instance.name if self.instance.pk else cleaned_data.get('name')
|
||||||
custom_filtering = self.cleaned_data['custom_filtering']
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
if filtering == self._meta.model.CUSTOM and not custom_filtering:
|
if name and local_domain:
|
||||||
raise forms.ValidationError({
|
try:
|
||||||
'custom_filtering': _("You didn't provide any custom filtering.")
|
addr = Address.objects.get(name=name, domain__name=local_domain, account_id=self.modeladmin.account.pk)
|
||||||
})
|
except Address.DoesNotExist:
|
||||||
return custom_filtering
|
pass
|
||||||
|
else:
|
||||||
|
if addr not in cleaned_data.get('addresses', []):
|
||||||
|
raise ValidationError({
|
||||||
|
'addresses': _("This mailbox matches local address '%s', "
|
||||||
|
"please make explicit this fact by selecting it.") % addr
|
||||||
|
})
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class MailboxChangeForm(UserChangeForm, MailboxForm):
|
class MailboxChangeForm(UserChangeForm, MailboxForm):
|
||||||
|
@ -73,5 +82,23 @@ class MailboxCreationForm(UserCreationForm, MailboxForm):
|
||||||
class AddressForm(forms.ModelForm):
|
class AddressForm(forms.ModelForm):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super(AddressForm, self).clean()
|
cleaned_data = super(AddressForm, self).clean()
|
||||||
if not cleaned_data.get('mailboxes', True) and not cleaned_data['forward']:
|
forward = cleaned_data.get('forward', '')
|
||||||
raise forms.ValidationError(_("Mailboxes or forward address should be provided."))
|
if not cleaned_data.get('mailboxes', True) and not forward:
|
||||||
|
raise ValidationError(_("Mailboxes or forward address should be provided."))
|
||||||
|
# Check if new addresse matches with a mbox because of having a local domain
|
||||||
|
if self.instance.pk:
|
||||||
|
name = self.instance.name
|
||||||
|
domain = self.instance.domain
|
||||||
|
else:
|
||||||
|
name = cleaned_data.get('name')
|
||||||
|
domain = cleaned_data.get('domain')
|
||||||
|
if domain and name and domain.name == settings.MAILBOXES_LOCAL_DOMAIN:
|
||||||
|
if name not in forward.split() and Mailbox.objects.filter(name=name).exists():
|
||||||
|
for mailbox in cleaned_data.get('mailboxes', []):
|
||||||
|
if mailbox.name == name:
|
||||||
|
return
|
||||||
|
raise ValidationError(
|
||||||
|
_("This address matches mailbox '%s', please make explicit this fact "
|
||||||
|
"by adding the mailbox on the mailboxes or forward field.") % name
|
||||||
|
)
|
||||||
|
return cleaned_data
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.core.validators import RegexValidator, ValidationError
|
from django.core.validators import RegexValidator, ValidationError
|
||||||
|
@ -59,6 +60,10 @@ class Mailbox(models.Model):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.custom_filtering and self.filtering != self.CUSTOM:
|
if self.custom_filtering and self.filtering != self.CUSTOM:
|
||||||
self.custom_filtering = ''
|
self.custom_filtering = ''
|
||||||
|
elif self.filtering == self.CUSTOM and not self.custom_filtering:
|
||||||
|
raise ValidationError({
|
||||||
|
'custom_filtering': _("Custom filtering is selected but not provided.")
|
||||||
|
})
|
||||||
|
|
||||||
def get_filtering(self):
|
def get_filtering(self):
|
||||||
name, content = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering]
|
name, content = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering]
|
||||||
|
@ -66,18 +71,6 @@ class Mailbox(models.Model):
|
||||||
content = content(self)
|
content = content(self)
|
||||||
return (name, content)
|
return (name, content)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
|
||||||
super(Mailbox, self).delete(*args, **kwargs)
|
|
||||||
# Cleanup related addresses
|
|
||||||
for address in Address.objects.filter(forward__regex=r'.*(^|\s)+%s($|\s)+.*' % self.name):
|
|
||||||
forward = address.forward.split()
|
|
||||||
forward.remove(self.name)
|
|
||||||
address.forward = ' '.join(forward)
|
|
||||||
if not address.destination:
|
|
||||||
address.delete()
|
|
||||||
else:
|
|
||||||
address.save()
|
|
||||||
|
|
||||||
def get_local_address(self):
|
def get_local_address(self):
|
||||||
if not settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN:
|
if not settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN:
|
||||||
raise AttributeError("Mailboxes do not have a defined local address domain.")
|
raise AttributeError("Mailboxes do not have a defined local address domain.")
|
||||||
|
@ -117,17 +110,30 @@ class Address(models.Model):
|
||||||
return ' '.join(destinations)
|
return ' '.join(destinations)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
errors = defaultdict(list)
|
||||||
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
|
if local_domain:
|
||||||
|
forwards = self.forward.split()
|
||||||
|
for ix, forward in enumerate(forwards):
|
||||||
|
if forward.endswith('@%s' % local_domain):
|
||||||
|
name = forward.split('@')[0]
|
||||||
|
if Mailbox.objects.filter(name=name).exists():
|
||||||
|
forwards[ix] = name
|
||||||
|
self.forward = ' '.join(forwards)
|
||||||
if self.account_id:
|
if self.account_id:
|
||||||
forward_errors = []
|
|
||||||
for mailbox in self.get_forward_mailboxes():
|
for mailbox in self.get_forward_mailboxes():
|
||||||
if mailbox.account_id == self.account_id:
|
if mailbox.account_id == self.account_id:
|
||||||
forward_errors.append(ValidationError(
|
errors['forward'].append(
|
||||||
_("Please use mailboxes field for '%s' mailbox.") % mailbox
|
_("Please use mailboxes field for '%s' mailbox.") % mailbox
|
||||||
))
|
)
|
||||||
if forward_errors:
|
if self.domain:
|
||||||
raise ValidationError({
|
for forward in self.forward.split():
|
||||||
'forward': forward_errors
|
if self.email == forward:
|
||||||
})
|
errors['forward'].append(
|
||||||
|
_("'%s' forwards to itself.") % forward
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
raise ValidationError(errors)
|
||||||
|
|
||||||
def get_forward_mailboxes(self):
|
def get_forward_mailboxes(self):
|
||||||
for forward in self.forward.split():
|
for forward in self.forward.split():
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
from django.db.models.signals import pre_save, post_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from . import settings
|
||||||
|
from .models import Mailbox, Address
|
||||||
|
|
||||||
|
|
||||||
|
# Admin bulk deletion doesn't call model.delete()
|
||||||
|
# So, signals are used instead of model method overriding
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=Mailbox, dispatch_uid='mailboxes.delete_forwards')
|
||||||
|
def delete_forwards(sender, *args, **kwargs):
|
||||||
|
# Cleanup related addresses
|
||||||
|
instance = kwargs['instance']
|
||||||
|
for address in Address.objects.filter(forward__regex=r'.*(^|\s)+%s($|\s)+.*' % instance.name):
|
||||||
|
forward = address.forward.split()
|
||||||
|
forward.remove(instance.name)
|
||||||
|
address.forward = ' '.join(forward)
|
||||||
|
if not address.destination:
|
||||||
|
address.delete()
|
||||||
|
else:
|
||||||
|
address.save()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=Mailbox, dispatch_uid='mailboxes.create_local_address')
|
||||||
|
def create_local_address(sender, *args, **kwargs):
|
||||||
|
mbox = kwargs['instance']
|
||||||
|
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||||
|
if not mbox.pk and local_domain:
|
||||||
|
Domain = Address._meta.get_field_by_name('domain')[0].rel.to
|
||||||
|
try:
|
||||||
|
domain = Domain.objects.get(name=local_domain)
|
||||||
|
except Domain.DoesNotExist:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
addr, created = Address.objects.get_or_create(
|
||||||
|
name=mbox.name, domain=domain, account_id=domain.account_id)
|
||||||
|
if created:
|
||||||
|
if domain.account_id == mbox.account_id:
|
||||||
|
addr.mailboxes.add(mbox)
|
||||||
|
else:
|
||||||
|
addr.forward = mbox.name
|
||||||
|
addr.save(update_fields=('forward',))
|
|
@ -94,7 +94,8 @@ class RouteAdmin(ExtendedModelAdmin):
|
||||||
|
|
||||||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||||
self.show_orchestration_disabled(request)
|
self.show_orchestration_disabled(request)
|
||||||
return super(RouteAdmin, self).changeform_view(request, object_id, form_url, extra_context)
|
return super(RouteAdmin, self).changeform_view(
|
||||||
|
request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
|
||||||
class BackendOperationInline(admin.TabularInline):
|
class BackendOperationInline(admin.TabularInline):
|
||||||
|
|
|
@ -28,11 +28,11 @@ class WebAppServiceMixin(object):
|
||||||
if context['under_construction_path']:
|
if context['under_construction_path']:
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""
|
||||||
# Set under construction if needed
|
# Set under construction if needed
|
||||||
if [[ $CREATED == 1 && ! $(ls -A %(app_path)s) ]]; then
|
if [[ $CREATED == 1 && ! $(ls -A %(app_path)s | head -n1) ]]; then
|
||||||
# Async wait 2 more seconds for other backends to lock app_path or cp under construction
|
# Async wait some seconds for other backends to lock app_path or cp under construction
|
||||||
nohup bash -c '
|
nohup bash -c '
|
||||||
sleep 2
|
sleep 2
|
||||||
if [[ ! $(ls -A %(app_path)s) ]]; then
|
if [[ ! $(ls -A %(app_path)s | head -n1) ]]; then
|
||||||
cp -r %(under_construction_path)s %(app_path)s
|
cp -r %(under_construction_path)s %(app_path)s
|
||||||
chown -R %(user)s:%(group)s %(app_path)s
|
chown -R %(user)s:%(group)s %(app_path)s
|
||||||
fi' &> /dev/null &
|
fi' &> /dev/null &
|
||||||
|
|
|
@ -4,34 +4,19 @@ from django.utils import timezone
|
||||||
from django.utils.translation import ungettext, ugettext as _
|
from django.utils.translation import ungettext, ugettext as _
|
||||||
|
|
||||||
|
|
||||||
def pluralize_year(n):
|
def verbose_time(n, units):
|
||||||
|
if n >= 5:
|
||||||
|
return _("{n} {units} ago").format(n=int(n), units=units)
|
||||||
return ungettext(
|
return ungettext(
|
||||||
_('{num:.1f} year{ago}'),
|
_("{n:.1f} {s_units} ago"),
|
||||||
_('{num:.1f} years{ago}'), n)
|
_("{n:.1f} {units} ago"), n
|
||||||
|
).format(n=n, units=units, s_units=units[:-1])
|
||||||
|
|
||||||
def pluralize_month(n):
|
|
||||||
return ungettext(
|
|
||||||
_('{num:.1f} month{ago}'),
|
|
||||||
_('{num:.1f} months{ago}'), n)
|
|
||||||
|
|
||||||
|
|
||||||
def pluralize_week(n):
|
|
||||||
return ungettext(
|
|
||||||
_('{num:.1f} week{ago}'),
|
|
||||||
_('{num:.1f} weeks {ago}'), n)
|
|
||||||
|
|
||||||
|
|
||||||
def pluralize_day(n):
|
|
||||||
return ungettext(
|
|
||||||
_('{num:.1f} day{ago}'),
|
|
||||||
_('{num:.1f} days{ago}'), n)
|
|
||||||
|
|
||||||
|
|
||||||
OLDER_CHUNKS = (
|
OLDER_CHUNKS = (
|
||||||
(365.0, pluralize_year),
|
(365.0, 'years'),
|
||||||
(30.0, pluralize_month),
|
(30.0, 'months'),
|
||||||
(7.0, pluralize_week),
|
(7.0, 'weeks'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,50 +37,34 @@ def naturaldatetime(date, show_seconds=False):
|
||||||
delta_midnight = today - date
|
delta_midnight = today - date
|
||||||
|
|
||||||
days = delta.days
|
days = delta.days
|
||||||
hours = int(round(delta.seconds / 3600, 0))
|
hours = float(delta.seconds) / 3600
|
||||||
minutes = delta.seconds / 60
|
minutes = float(delta.seconds) / 60
|
||||||
seconds = delta.seconds
|
seconds = delta.seconds
|
||||||
|
|
||||||
ago = " ago"
|
|
||||||
if days < 0:
|
|
||||||
ago = ""
|
|
||||||
days = abs(days)
|
days = abs(days)
|
||||||
|
|
||||||
if days == 0:
|
if days == 0:
|
||||||
if hours == 0:
|
if int(hours) == 0:
|
||||||
if minutes >= 1 or not show_seconds:
|
if minutes >= 1 or not show_seconds:
|
||||||
minutes = float(seconds)/60
|
return verbose_time(minutes, 'minutes')
|
||||||
return ungettext(
|
|
||||||
_("{minutes:.1f} minute{ago}"),
|
|
||||||
_("{minutes:.1f} minutes{ago}"), minutes
|
|
||||||
).format(minutes=minutes, ago=ago)
|
|
||||||
else:
|
else:
|
||||||
return ungettext(
|
return verbose_time(seconds, 'seconds')
|
||||||
_("{seconds} second{ago}"),
|
|
||||||
_("{seconds} seconds{ago}"), seconds
|
|
||||||
).format(seconds=seconds, ago=ago)
|
|
||||||
else:
|
else:
|
||||||
hours = float(minutes)/60
|
return verbose_time(hours, 'hours')
|
||||||
return ungettext(
|
|
||||||
_("{hours:.1f} hour{ago}"),
|
|
||||||
_("{hours:.1f} hours{ago}"), hours
|
|
||||||
).format(hours=hours, ago=ago)
|
|
||||||
|
|
||||||
if delta_midnight.days == 0:
|
if delta_midnight.days == 0:
|
||||||
date = timezone.localtime(date)
|
date = timezone.localtime(date)
|
||||||
return _("yesterday at {time}").format(time=date.strftime('%H:%M'))
|
return _("yesterday at {time}").format(time=date.strftime('%H:%M'))
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
for chunk, pluralizefun in OLDER_CHUNKS:
|
for chunk, units in OLDER_CHUNKS:
|
||||||
if days < 7.0:
|
if days < 7.0:
|
||||||
count = days + float(hours)/24
|
count = days + float(hours)/24
|
||||||
fmt = pluralize_day(count)
|
return verbose_time(count, 'days')
|
||||||
return fmt.format(num=count, ago=ago)
|
|
||||||
if days >= chunk:
|
if days >= chunk:
|
||||||
count = (delta_midnight.days + 1) / chunk
|
count = (delta_midnight.days + 1) / chunk
|
||||||
count = abs(count)
|
count = abs(count)
|
||||||
fmt = pluralizefun(count)
|
return verbose_time(count, units)
|
||||||
return fmt.format(num=count, ago=ago)
|
|
||||||
|
|
||||||
|
|
||||||
def naturaldate(date):
|
def naturaldate(date):
|
||||||
|
|
Loading…
Reference in New Issue