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?
|
||||
* 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
|
||||
|
||||
* 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
# 'django.middleware.locale.LocaleMiddleware'
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
|
|
|
@ -76,24 +76,22 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
|||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
|
||||
return super(AccountAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
def change_view(self, request, object_id, form_url='', extra_context=None):
|
||||
if request.method == 'GET':
|
||||
account = self.get_object(request, unquote(object_id))
|
||||
if not account.is_active:
|
||||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||
if not add:
|
||||
if request.method == 'GET' and not obj.is_active:
|
||||
messages.warning(request, 'This account is disabled.')
|
||||
context = {
|
||||
'services': sorted(
|
||||
[model._meta for model in services.get() if model is not Account],
|
||||
key=lambda i: i.verbose_name_plural.lower()
|
||||
),
|
||||
'accounts': sorted(
|
||||
[model._meta for model in accounts.get() if model is not Account],
|
||||
key=lambda i: i.verbose_name_plural.lower()
|
||||
)
|
||||
}
|
||||
context.update(extra_context or {})
|
||||
return super(AccountAdmin, self).change_view(
|
||||
request, object_id, form_url=form_url, extra_context=context)
|
||||
context.update({
|
||||
'services': sorted(
|
||||
[model._meta for model in services.get() if model is not Account],
|
||||
key=lambda i: i.verbose_name_plural.lower()
|
||||
),
|
||||
'accounts': sorted(
|
||||
[model._meta for model in accounts.get() if model is not Account],
|
||||
key=lambda i: i.verbose_name_plural.lower()
|
||||
)
|
||||
})
|
||||
return super(AccountAdmin, self).render_change_form(
|
||||
request, context, add, change, form_url, obj)
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
|
||||
|
|
|
@ -82,19 +82,16 @@ class LogEntryAdmin(admin.ModelAdmin):
|
|||
content_object_link.admin_order_field = 'object_repr'
|
||||
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 """
|
||||
context = {}
|
||||
if 'edit' in request.GET.urlencode():
|
||||
obj = self.get_object(request, unquote(object_id))
|
||||
context = {
|
||||
if not add and 'edit' in request.GET.urlencode():
|
||||
context.update({
|
||||
'rel_opts': obj.content_type.model_class()._meta,
|
||||
'object': obj,
|
||||
}
|
||||
context.update(extra_context or {})
|
||||
return super(LogEntryAdmin, self).changeform_view(
|
||||
request, object_id, form_url, extra_context=context)
|
||||
|
||||
})
|
||||
return super(LogEntryAdmin, self).render_change_form(
|
||||
request, context, add, change, form_url, obj)
|
||||
|
||||
def response_change(self, request, obj):
|
||||
""" save and continue preserve edit query string """
|
||||
response = super(LogEntryAdmin, self).response_change(request, obj)
|
||||
|
|
|
@ -2,9 +2,11 @@ import copy
|
|||
from urllib.parse import parse_qs
|
||||
|
||||
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.functions import Concat
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||
|
@ -122,8 +124,27 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
|||
search_term = search_term.replace('@', ' ')
|
||||
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):
|
||||
""" save hacky mailbox.addresses """
|
||||
""" save hacky mailbox.addresses and local domain clashing """
|
||||
super(MailboxAdmin, self).save_model(request, obj, form, change)
|
||||
obj.addresses = form.cleaned_data['addresses']
|
||||
|
||||
|
|
|
@ -11,3 +11,4 @@ class MailboxesConfig(AppConfig):
|
|||
from .models import Mailbox, Address
|
||||
services.register(Mailbox, icon='email.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 . import settings
|
||||
from .models import Address
|
||||
from .models import Address, Mailbox
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -137,97 +137,97 @@ class UNIXUserMaildirBackend(SieveFilteringMixin, ServiceController):
|
|||
return context
|
||||
|
||||
|
||||
class DovecotPostfixPasswdVirtualUserBackend(SieveFilteringMixin, ServiceController):
|
||||
"""
|
||||
WARNING: This backends is not fully implemented
|
||||
"""
|
||||
DEFAULT_GROUP = 'postfix'
|
||||
|
||||
verbose_name = _("Dovecot-Postfix virtualuser")
|
||||
model = 'mailboxes.Mailbox'
|
||||
|
||||
def set_user(self, context):
|
||||
self.append(textwrap.dedent("""
|
||||
if grep '^%(user)s:' %(passwd_path)s > /dev/null ; then
|
||||
sed -i 's#^%(user)s:.*#%(passwd)s#' %(passwd_path)s
|
||||
else
|
||||
echo '%(passwd)s' >> %(passwd_path)s
|
||||
fi""") % context
|
||||
)
|
||||
self.append("mkdir -p %(home)s" % context)
|
||||
self.append("chown %(uid)s:%(gid)s %(home)s" % context)
|
||||
|
||||
def set_mailbox(self, context):
|
||||
self.append(textwrap.dedent("""
|
||||
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
|
||||
UPDATED_VIRTUAL_MAILBOX_MAPS=1
|
||||
fi""") % context
|
||||
)
|
||||
|
||||
def save(self, mailbox):
|
||||
context = self.get_context(mailbox)
|
||||
self.set_user(context)
|
||||
self.set_mailbox(context)
|
||||
self.generate_filter(mailbox, context)
|
||||
|
||||
def delete(self, mailbox):
|
||||
context = self.get_context(mailbox)
|
||||
self.append(textwrap.dedent("""
|
||||
nohup bash -c 'sleep 2 && killall -u %(uid)s -s KILL' &> /dev/null &
|
||||
killall -u %(uid)s || true
|
||||
sed -i '/^%(user)s:.*/d' %(passwd_path)s
|
||||
sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s
|
||||
UPDATED_VIRTUAL_MAILBOX_MAPS=1""") % context
|
||||
)
|
||||
if context['deleted_home']:
|
||||
self.append("mv %(home)s %(deleted_home)s || exit_code=$?" % context)
|
||||
else:
|
||||
self.append("rm -fr %(home)s" % context)
|
||||
|
||||
def get_extra_fields(self, mailbox, context):
|
||||
context['quota'] = self.get_quota(mailbox)
|
||||
return 'userdb_mail=maildir:~/Maildir {quota}'.format(**context)
|
||||
|
||||
def get_quota(self, mailbox):
|
||||
try:
|
||||
quota = mailbox.resources.disk.allocated
|
||||
except (AttributeError, ObjectDoesNotExist):
|
||||
return ''
|
||||
unit = mailbox.resources.disk.unit[0].upper()
|
||||
return 'userdb_quota_rule=*:bytes=%i%s' % (quota, unit)
|
||||
|
||||
def commit(self):
|
||||
context = {
|
||||
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
|
||||
}
|
||||
self.append(textwrap.dedent("""
|
||||
[[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && {
|
||||
postmap %(virtual_mailbox_maps)s
|
||||
}""") % context
|
||||
)
|
||||
|
||||
def get_context(self, mailbox):
|
||||
context = {
|
||||
'name': mailbox.name,
|
||||
'user': mailbox.name,
|
||||
'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
|
||||
'uid': 10000 + mailbox.pk,
|
||||
'gid': 10000 + mailbox.pk,
|
||||
'group': self.DEFAULT_GROUP,
|
||||
'quota': self.get_quota(mailbox),
|
||||
'passwd_path': settings.MAILBOXES_PASSWD_PATH,
|
||||
'home': mailbox.get_home(),
|
||||
'banner': self.get_banner(),
|
||||
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH,
|
||||
'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
|
||||
}
|
||||
context['extra_fields'] = self.get_extra_fields(mailbox, context)
|
||||
context.update({
|
||||
'passwd': '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context),
|
||||
'deleted_home': settings.MAILBOXES_MOVE_ON_DELETE_PATH % context,
|
||||
})
|
||||
return context
|
||||
#class DovecotPostfixPasswdVirtualUserBackend(SieveFilteringMixin, ServiceController):
|
||||
# """
|
||||
# WARNING: This backends is not fully implemented
|
||||
# """
|
||||
# DEFAULT_GROUP = 'postfix'
|
||||
#
|
||||
# verbose_name = _("Dovecot-Postfix virtualuser")
|
||||
# model = 'mailboxes.Mailbox'
|
||||
#
|
||||
# def set_user(self, context):
|
||||
# self.append(textwrap.dedent("""
|
||||
# if grep '^%(user)s:' %(passwd_path)s > /dev/null ; then
|
||||
# sed -i 's#^%(user)s:.*#%(passwd)s#' %(passwd_path)s
|
||||
# else
|
||||
# echo '%(passwd)s' >> %(passwd_path)s
|
||||
# fi""") % context
|
||||
# )
|
||||
# self.append("mkdir -p %(home)s" % context)
|
||||
# self.append("chown %(uid)s:%(gid)s %(home)s" % context)
|
||||
#
|
||||
# def set_mailbox(self, context):
|
||||
# self.append(textwrap.dedent("""
|
||||
# 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
|
||||
# UPDATED_VIRTUAL_MAILBOX_MAPS=1
|
||||
# fi""") % context
|
||||
# )
|
||||
#
|
||||
# def save(self, mailbox):
|
||||
# context = self.get_context(mailbox)
|
||||
# self.set_user(context)
|
||||
# self.set_mailbox(context)
|
||||
# self.generate_filter(mailbox, context)
|
||||
#
|
||||
# def delete(self, mailbox):
|
||||
# context = self.get_context(mailbox)
|
||||
# self.append(textwrap.dedent("""
|
||||
# nohup bash -c 'sleep 2 && killall -u %(uid)s -s KILL' &> /dev/null &
|
||||
# killall -u %(uid)s || true
|
||||
# sed -i '/^%(user)s:.*/d' %(passwd_path)s
|
||||
# sed -i '/^%(user)s@%(mailbox_domain)s\s.*/d' %(virtual_mailbox_maps)s
|
||||
# UPDATED_VIRTUAL_MAILBOX_MAPS=1""") % context
|
||||
# )
|
||||
# if context['deleted_home']:
|
||||
# self.append("mv %(home)s %(deleted_home)s || exit_code=$?" % context)
|
||||
# else:
|
||||
# self.append("rm -fr %(home)s" % context)
|
||||
#
|
||||
# def get_extra_fields(self, mailbox, context):
|
||||
# context['quota'] = self.get_quota(mailbox)
|
||||
# return 'userdb_mail=maildir:~/Maildir {quota}'.format(**context)
|
||||
#
|
||||
# def get_quota(self, mailbox):
|
||||
# try:
|
||||
# quota = mailbox.resources.disk.allocated
|
||||
# except (AttributeError, ObjectDoesNotExist):
|
||||
# return ''
|
||||
# unit = mailbox.resources.disk.unit[0].upper()
|
||||
# return 'userdb_quota_rule=*:bytes=%i%s' % (quota, unit)
|
||||
#
|
||||
# def commit(self):
|
||||
# context = {
|
||||
# 'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
|
||||
# }
|
||||
# self.append(textwrap.dedent("""
|
||||
# [[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && {
|
||||
# postmap %(virtual_mailbox_maps)s
|
||||
# }""") % context
|
||||
# )
|
||||
#
|
||||
# def get_context(self, mailbox):
|
||||
# context = {
|
||||
# 'name': mailbox.name,
|
||||
# 'user': mailbox.name,
|
||||
# 'password': mailbox.password if mailbox.active else '*%s' % mailbox.password,
|
||||
# 'uid': 10000 + mailbox.pk,
|
||||
# 'gid': 10000 + mailbox.pk,
|
||||
# 'group': self.DEFAULT_GROUP,
|
||||
# 'quota': self.get_quota(mailbox),
|
||||
# 'passwd_path': settings.MAILBOXES_PASSWD_PATH,
|
||||
# 'home': mailbox.get_home(),
|
||||
# 'banner': self.get_banner(),
|
||||
# 'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH,
|
||||
# 'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
|
||||
# }
|
||||
# context['extra_fields'] = self.get_extra_fields(mailbox, context)
|
||||
# context.update({
|
||||
# 'passwd': '{user}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context),
|
||||
# 'deleted_home': settings.MAILBOXES_MOVE_ON_DELETE_PATH % context,
|
||||
# })
|
||||
# return context
|
||||
|
||||
|
||||
class PostfixAddressVirtualDomainBackend(ServiceController):
|
||||
|
@ -301,6 +301,7 @@ class PostfixAddressVirtualDomainBackend(ServiceController):
|
|||
def get_context(self, address):
|
||||
context = self.get_context_files()
|
||||
context.update({
|
||||
'name': address.name,
|
||||
'domain': address.domain,
|
||||
'email': address.email,
|
||||
'local_domain': settings.MAILBOXES_LOCAL_DOMAIN,
|
||||
|
@ -319,10 +320,19 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend):
|
|||
'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):
|
||||
destination = address.destination
|
||||
if destination:
|
||||
context['destination'] = destination
|
||||
context['destination'] = address.destination
|
||||
if not self.is_implicit_entry(context):
|
||||
self.append(textwrap.dedent("""
|
||||
# Set virtual alias entry for %(email)s
|
||||
LINE='%(email)s\t%(destination)s'
|
||||
|
@ -338,7 +348,12 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend):
|
|||
fi
|
||||
fi""") % context)
|
||||
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)
|
||||
# Virtual mailbox stuff
|
||||
# destination = []
|
||||
|
@ -350,7 +365,7 @@ class PostfixAddressBackend(PostfixAddressVirtualDomainBackend):
|
|||
# destination.append(forward)
|
||||
|
||||
def exclude_virtual_alias_maps(self, context):
|
||||
self.append(textwrap.dedent("""
|
||||
self.append(textwrap.dedent("""\
|
||||
# Remove %(email)s virtual alias entry
|
||||
if grep '^%(email)s\s' %(virtual_alias_maps)s > /dev/null; then
|
||||
sed -i '/^%(email)s\s/d' %(virtual_alias_maps)s
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from django import forms
|
||||
from django.contrib.admin import widgets
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.forms import UserCreationForm, UserChangeForm
|
||||
from orchestra.utils.python import AttrDict
|
||||
|
||||
from .models import Address
|
||||
from . import settings
|
||||
from .models import Address, Mailbox
|
||||
|
||||
|
||||
class MailboxForm(forms.ModelForm):
|
||||
|
@ -37,21 +39,28 @@ class MailboxForm(forms.ModelForm):
|
|||
return mark_safe(output)
|
||||
self.fields['addresses'].widget.render = render
|
||||
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
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields['addresses'].initial = self.instance.addresses.all()
|
||||
|
||||
def clean_custom_filtering(self):
|
||||
# TODO move to model.clean?
|
||||
filtering = self.cleaned_data['filtering']
|
||||
custom_filtering = self.cleaned_data['custom_filtering']
|
||||
if filtering == self._meta.model.CUSTOM and not custom_filtering:
|
||||
raise forms.ValidationError({
|
||||
'custom_filtering': _("You didn't provide any custom filtering.")
|
||||
})
|
||||
return custom_filtering
|
||||
def clean(self):
|
||||
cleaned_data = super(MailboxForm, self).clean()
|
||||
name = self.instance.name if self.instance.pk else cleaned_data.get('name')
|
||||
local_domain = settings.MAILBOXES_LOCAL_DOMAIN
|
||||
if name and local_domain:
|
||||
try:
|
||||
addr = Address.objects.get(name=name, domain__name=local_domain, account_id=self.modeladmin.account.pk)
|
||||
except Address.DoesNotExist:
|
||||
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):
|
||||
|
@ -73,5 +82,23 @@ class MailboxCreationForm(UserCreationForm, MailboxForm):
|
|||
class AddressForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
cleaned_data = super(AddressForm, self).clean()
|
||||
if not cleaned_data.get('mailboxes', True) and not cleaned_data['forward']:
|
||||
raise forms.ValidationError(_("Mailboxes or forward address should be provided."))
|
||||
forward = cleaned_data.get('forward', '')
|
||||
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
|
||||
from collections import defaultdict
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core.validators import RegexValidator, ValidationError
|
||||
|
@ -59,6 +60,10 @@ class Mailbox(models.Model):
|
|||
def clean(self):
|
||||
if self.custom_filtering and self.filtering != self.CUSTOM:
|
||||
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):
|
||||
name, content = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering]
|
||||
|
@ -66,18 +71,6 @@ class Mailbox(models.Model):
|
|||
content = content(self)
|
||||
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):
|
||||
if not settings.MAILBOXES_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)
|
||||
|
||||
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:
|
||||
forward_errors = []
|
||||
for mailbox in self.get_forward_mailboxes():
|
||||
if mailbox.account_id == self.account_id:
|
||||
forward_errors.append(ValidationError(
|
||||
errors['forward'].append(
|
||||
_("Please use mailboxes field for '%s' mailbox.") % mailbox
|
||||
))
|
||||
if forward_errors:
|
||||
raise ValidationError({
|
||||
'forward': forward_errors
|
||||
})
|
||||
)
|
||||
if self.domain:
|
||||
for forward in self.forward.split():
|
||||
if self.email == forward:
|
||||
errors['forward'].append(
|
||||
_("'%s' forwards to itself.") % forward
|
||||
)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
def get_forward_mailboxes(self):
|
||||
for forward in self.forward.split():
|
||||
|
|
43
orchestra/contrib/mailboxes/signals.py
Normal file
43
orchestra/contrib/mailboxes/signals.py
Normal file
|
@ -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):
|
||||
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):
|
||||
|
|
|
@ -28,11 +28,11 @@ class WebAppServiceMixin(object):
|
|||
if context['under_construction_path']:
|
||||
self.append(textwrap.dedent("""
|
||||
# Set under construction if needed
|
||||
if [[ $CREATED == 1 && ! $(ls -A %(app_path)s) ]]; then
|
||||
# Async wait 2 more seconds for other backends to lock app_path or cp under construction
|
||||
if [[ $CREATED == 1 && ! $(ls -A %(app_path)s | head -n1) ]]; then
|
||||
# Async wait some seconds for other backends to lock app_path or cp under construction
|
||||
nohup bash -c '
|
||||
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
|
||||
chown -R %(user)s:%(group)s %(app_path)s
|
||||
fi' &> /dev/null &
|
||||
|
|
|
@ -4,34 +4,19 @@ from django.utils import timezone
|
|||
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(
|
||||
_('{num:.1f} year{ago}'),
|
||||
_('{num:.1f} years{ago}'), n)
|
||||
|
||||
|
||||
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)
|
||||
_("{n:.1f} {s_units} ago"),
|
||||
_("{n:.1f} {units} ago"), n
|
||||
).format(n=n, units=units, s_units=units[:-1])
|
||||
|
||||
|
||||
OLDER_CHUNKS = (
|
||||
(365.0, pluralize_year),
|
||||
(30.0, pluralize_month),
|
||||
(7.0, pluralize_week),
|
||||
(365.0, 'years'),
|
||||
(30.0, 'months'),
|
||||
(7.0, 'weeks'),
|
||||
)
|
||||
|
||||
|
||||
|
@ -52,50 +37,34 @@ def naturaldatetime(date, show_seconds=False):
|
|||
delta_midnight = today - date
|
||||
|
||||
days = delta.days
|
||||
hours = int(round(delta.seconds / 3600, 0))
|
||||
minutes = delta.seconds / 60
|
||||
hours = float(delta.seconds) / 3600
|
||||
minutes = float(delta.seconds) / 60
|
||||
seconds = delta.seconds
|
||||
|
||||
ago = " ago"
|
||||
if days < 0:
|
||||
ago = ""
|
||||
days = abs(days)
|
||||
|
||||
if days == 0:
|
||||
if hours == 0:
|
||||
if int(hours) == 0:
|
||||
if minutes >= 1 or not show_seconds:
|
||||
minutes = float(seconds)/60
|
||||
return ungettext(
|
||||
_("{minutes:.1f} minute{ago}"),
|
||||
_("{minutes:.1f} minutes{ago}"), minutes
|
||||
).format(minutes=minutes, ago=ago)
|
||||
return verbose_time(minutes, 'minutes')
|
||||
else:
|
||||
return ungettext(
|
||||
_("{seconds} second{ago}"),
|
||||
_("{seconds} seconds{ago}"), seconds
|
||||
).format(seconds=seconds, ago=ago)
|
||||
return verbose_time(seconds, 'seconds')
|
||||
else:
|
||||
hours = float(minutes)/60
|
||||
return ungettext(
|
||||
_("{hours:.1f} hour{ago}"),
|
||||
_("{hours:.1f} hours{ago}"), hours
|
||||
).format(hours=hours, ago=ago)
|
||||
return verbose_time(hours, 'hours')
|
||||
|
||||
if delta_midnight.days == 0:
|
||||
date = timezone.localtime(date)
|
||||
return _("yesterday at {time}").format(time=date.strftime('%H:%M'))
|
||||
|
||||
count = 0
|
||||
for chunk, pluralizefun in OLDER_CHUNKS:
|
||||
for chunk, units in OLDER_CHUNKS:
|
||||
if days < 7.0:
|
||||
count = days + float(hours)/24
|
||||
fmt = pluralize_day(count)
|
||||
return fmt.format(num=count, ago=ago)
|
||||
return verbose_time(count, 'days')
|
||||
if days >= chunk:
|
||||
count = (delta_midnight.days + 1) / chunk
|
||||
count = abs(count)
|
||||
fmt = pluralizefun(count)
|
||||
return fmt.format(num=count, ago=ago)
|
||||
return verbose_time(count, units)
|
||||
|
||||
|
||||
def naturaldate(date):
|
||||
|
|
Loading…
Reference in a new issue