Random fixes

This commit is contained in:
Marc Aymerich 2014-10-30 16:34:02 +00:00
parent d0c7c760af
commit c4e8c07311
25 changed files with 469 additions and 205 deletions

View file

@ -160,3 +160,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* prevent adding local email addresses on account.contacts account.email * prevent adding local email addresses on account.contacts account.email
* Resource monitoring without ROUTE alert or explicit error * Resource monitoring without ROUTE alert or explicit error
* Domain validation has to be done with injected records and subdomains

View file

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('systemusers', '__first__'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')),
('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and ./-/_ only.', unique=True, max_length=64, verbose_name='username', validators=[django.core.validators.RegexValidator(b'^[\\w.-]+$', 'Enter a valid username.', b'invalid')])),
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
('email', models.EmailField(help_text='Used for password recovery', max_length=75, verbose_name='email address')),
('type', models.CharField(default=b'INDIVIDUAL', max_length=32, verbose_name='type', choices=[(b'INDIVIDUAL', 'Individual'), (b'ASSOCIATION', 'Association'), (b'CUSTOMER', 'Customer'), (b'STAFF', 'Staff')])),
('language', models.CharField(default=b'ca', max_length=2, verbose_name='language', choices=[(b'ca', 'Catalan'), (b'es', 'Spanish'), (b'en', 'English')])),
('comments', models.TextField(max_length=256, verbose_name='comments', blank=True)),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('main_systemuser', models.ForeignKey(related_name='accounts_main', to='systemusers.SystemUser', null=True)),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
]

View file

@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='account',
name='first_name',
),
migrations.RemoveField(
model_name='account',
name='last_name',
),
migrations.AddField(
model_name='account',
name='full_name',
field=models.CharField(default='', max_length=30, verbose_name='full name'),
preserve_default=False,
),
migrations.AddField(
model_name='account',
name='short_name',
field=models.CharField(default='', max_length=30, verbose_name='short name', blank=True),
preserve_default=False,
),
]

View file

@ -14,13 +14,13 @@ from . import settings
class Account(auth.AbstractBaseUser): class Account(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 ./-/_ only."), help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.RegexValidator(r'^[\w.-]+$', validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')]) _("Enter a valid username."), 'invalid')])
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True, main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
related_name='accounts_main') related_name='accounts_main', editable=False)
short_name = models.CharField(_("short name"), max_length=30, blank=True) short_name = models.CharField(_("short name"), max_length=64, blank=True)
full_name = models.CharField(_("full name"), max_length=30) full_name = models.CharField(_("full name"), max_length=256)
email = models.EmailField(_('email address'), help_text=_("Used for password recovery")) email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES, type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE) max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE)

View file

@ -15,7 +15,7 @@ ACCOUNTS_DEFAULT_TYPE = getattr(settings, 'ACCOUNTS_DEFAULT_TYPE', 'INDIVIDUAL')
ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', ( ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', (
('en', _('English')), ('EN', _('English')),
)) ))
@ -23,7 +23,7 @@ ACCOUNTS_SYSTEMUSER_MODEL = getattr(settings, 'ACCOUNTS_SYSTEMUSER_MODEL',
'systemusers.SystemUser') 'systemusers.SystemUser')
ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en') ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'EN')
ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1) ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1)

View file

@ -1,6 +1,6 @@
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.core.validators import ValidationError from django.core.validators import ValidationError, RegexValidator
from django.db import models from django.db import models
from django.template import loader, Context from django.template import loader, Context
from django.utils import timezone from django.utils import timezone
@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.models import Account from orchestra.apps.accounts.models import Account
from orchestra.apps.contacts.models import Contact from orchestra.apps.contacts.models import Contact
from orchestra.core import accounts from orchestra.core import accounts, validators
from orchestra.utils.html import html_to_pdf from orchestra.utils.html import html_to_pdf
from . import settings from . import settings
@ -24,8 +24,11 @@ class BillContact(models.Model):
address = models.TextField(_("address")) address = models.TextField(_("address"))
city = models.CharField(_("city"), max_length=128, city = models.CharField(_("city"), max_length=128,
default=settings.BILLS_CONTACT_DEFAULT_CITY) default=settings.BILLS_CONTACT_DEFAULT_CITY)
zipcode = models.PositiveIntegerField(_("zip code")) zipcode = models.CharField(_("zip code"), max_length=10,
validators=[RegexValidator(r'^[0-9A-Z]{3,10}$',
_("Enter a valid zipcode."), 'invalid')])
country = models.CharField(_("country"), max_length=20, country = models.CharField(_("country"), max_length=20,
choices=settings.BILLS_CONTACT_COUNTRIES,
default=settings.BILLS_CONTACT_DEFAULT_COUNTRY) default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)
vat = models.CharField(_("VAT number"), max_length=64) vat = models.CharField(_("VAT number"), max_length=64)
@ -35,6 +38,12 @@ class BillContact(models.Model):
def get_name(self): def get_name(self):
return self.name or self.account.get_full_name() return self.name or self.account.get_full_name()
def clean(self):
self.vat = self.vat.strip()
self.city = self.city.strip()
validators.validate_vat(self.vat, self.country)
validators.validate_zipcode(self.zipcode, self.country)
class BillManager(models.Manager): class BillManager(models.Manager):
def get_queryset(self): def get_queryset(self):

View file

@ -1,4 +1,5 @@
from django.conf import settings from django.conf import settings
from django_countries import data
BILLS_NUMBER_LENGTH = getattr(settings, 'BILLS_NUMBER_LENGTH', 4) BILLS_NUMBER_LENGTH = getattr(settings, 'BILLS_NUMBER_LENGTH', 4)
@ -57,4 +58,9 @@ BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')
BILLS_CONTACT_DEFAULT_CITY = getattr(settings, 'BILLS_CONTACT_DEFAULT_CITY', 'Barcelona') BILLS_CONTACT_DEFAULT_CITY = getattr(settings, 'BILLS_CONTACT_DEFAULT_CITY', 'Barcelona')
BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'Spain') BILLS_CONTACT_COUNTRIES = getattr(settings, 'BILLS_CONTACT_COUNTRIES', data.COUNTRIES)
BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'ES')

View file

@ -1,12 +1,17 @@
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
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 orchestra.core import accounts from orchestra.core import accounts, validators
from orchestra.models.fields import MultiSelectField from orchestra.models.fields import MultiSelectField
from . import settings from . import settings
validate_phone = lambda p: validators.validate_phone(p, settings.CONTACTS_DEFAULT_COUNTRY)
class ContactQuerySet(models.QuerySet): class ContactQuerySet(models.QuerySet):
def filter(self, *args, **kwargs): def filter(self, *args, **kwargs):
usages = kwargs.pop('email_usages', []) usages = kwargs.pop('email_usages', [])
@ -37,14 +42,17 @@ class Contact(models.Model):
email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True, email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True,
choices=EMAIL_USAGES, choices=EMAIL_USAGES,
default=settings.CONTACTS_DEFAULT_EMAIL_USAGES) default=settings.CONTACTS_DEFAULT_EMAIL_USAGES)
phone = models.CharField(_("phone"), max_length=32, blank=True) phone = models.CharField(_("phone"), max_length=32, blank=True,
phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True) validators=[validate_phone])
phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True,
validators=[validate_phone])
address = models.TextField(_("address"), blank=True) address = models.TextField(_("address"), blank=True)
city = models.CharField(_("city"), max_length=128, blank=True, city = models.CharField(_("city"), max_length=128, blank=True)
default=settings.CONTACTS_DEFAULT_CITY) zipcode = models.CharField(_("zip code"), max_length=10, blank=True,
zipcode = models.PositiveIntegerField(_("zip code"), null=True, blank=True) validators=[RegexValidator(r'^[0-9,A-Z]{3,10}$',
_("Enter a valid zipcode."), 'invalid')])
country = models.CharField(_("country"), max_length=20, blank=True, country = models.CharField(_("country"), max_length=20, blank=True,
default=settings.CONTACTS_DEFAULT_COUNTRY) choices=settings.CONTACTS_COUNTRIES)
def __unicode__(self): def __unicode__(self):
return self.short_name return self.short_name
@ -57,6 +65,12 @@ class Contact(models.Model):
self.address = self.address.strip() self.address = self.address.strip()
self.city = self.city.strip() self.city = self.city.strip()
self.country = self.country.strip() self.country = self.country.strip()
if self.address and not (self.city and self.zipcode and self.country):
raise ValidationError(_("City, zipcode and country must be provided when address is provided."))
if self.zipcode and not self.country:
raise ValidationError(_("Country must be provided when zipcode is provided."))
elif self.zipcode and self.country:
validators.validate_zipcode(self.zipcode, self.country)
accounts.register(Contact) accounts.register(Contact)

View file

@ -1,4 +1,5 @@
from django.conf import settings from django.conf import settings
from django_countries import data
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES', CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
@ -9,7 +10,7 @@ CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES
CONTACTS_DEFAULT_CITY = getattr(settings, 'CONTACTS_DEFAULT_CITY', 'Barcelona') CONTACTS_DEFAULT_CITY = getattr(settings, 'CONTACTS_DEFAULT_CITY', 'Barcelona')
CONTACTS_DEFAULT_PROVINCE = getattr(settings, 'CONTACTS_DEFAULT_PROVINCE', 'Barcelona') CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', data.COUNTRIES)
CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'Spain') CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'ES')

View file

@ -2,8 +2,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services from orchestra.core import services
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address, from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii
validate_hostname, validate_ascii)
from orchestra.utils.python import AttrDict from orchestra.utils.python import AttrDict
from . import settings, validators, utils from . import settings, validators, utils
@ -11,11 +10,11 @@ from . import settings, validators, utils
class Domain(models.Model): class Domain(models.Model):
name = models.CharField(_("name"), max_length=256, unique=True, name = models.CharField(_("name"), max_length=256, unique=True,
validators=[validate_hostname, validators.validate_allowed_domain], validators=[validators.validate_domain_name, validators.validate_allowed_domain],
help_text=_("Domain or subdomain name.")) help_text=_("Domain or subdomain name."))
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='domains', blank=True, help_text=_("Automatically selected for subdomains.")) related_name='domains', blank=True, help_text=_("Automatically selected for subdomains."))
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains') top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains', editable=False)
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial,
help_text=_("Serial number")) help_text=_("Serial number"))

View file

@ -4,6 +4,7 @@ import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core.validators import validate_hostname
from orchestra.utils import paths from orchestra.utils import paths
from orchestra.utils.system import run from orchestra.utils.system import run
@ -23,6 +24,15 @@ def validate_allowed_domain(value):
raise ValidationError(_("This domain name is not allowed")) raise ValidationError(_("This domain name is not allowed"))
def validate_domain_name(value):
# SRV records may use '_' in the domain name
value = value.lstrip('*.').replace('_', '')
try:
validate_hostname(value)
except ValidationError:
raise ValidationError(_("Not a valid domain name."))
def validate_zone_interval(value): def validate_zone_interval(value):
try: try:
int(value) int(value)

View file

@ -24,6 +24,11 @@ class PaymentMethod(plugins.Plugin):
plugins.append(import_class(cls)) plugins.append(import_class(cls))
return plugins return plugins
@classmethod
def clean_data(cls, data):
""" model clean """
return data
def get_form(self): def get_form(self):
self.form.plugin = self self.form.plugin = self
self.form.plugin_field = 'method' self.form.plugin_field = 'method'

View file

@ -24,24 +24,12 @@ class SEPADirectDebitForm(PluginDataForm):
name = forms.CharField(max_length=128, label=_("Name"), name = forms.CharField(max_length=128, label=_("Name"),
widget=forms.TextInput(attrs={'size': '50'})) widget=forms.TextInput(attrs={'size': '50'}))
def clean_iban(self):
return self.cleaned_data['iban'].strip()
def clean_name(self):
return self.cleaned_data['name'].strip()
class SEPADirectDebitSerializer(serializers.Serializer): class SEPADirectDebitSerializer(serializers.Serializer):
iban = serializers.CharField(label='IBAN', validators=[IBANValidator()], iban = serializers.CharField(label='IBAN', validators=[IBANValidator()],
min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34) min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34)
name = serializers.CharField(label=_("Name"), max_length=128) name = serializers.CharField(label=_("Name"), max_length=128)
def clean_iban(self, attrs, source):
return attrs[source].strip()
def clean_name(self, attrs, source):
return attrs[source].strip()
class SEPADirectDebit(PaymentMethod): class SEPADirectDebit(PaymentMethod):
verbose_name = _("SEPA Direct Debit") verbose_name = _("SEPA Direct Debit")
@ -56,6 +44,13 @@ class SEPADirectDebit(PaymentMethod):
return _("This bill will been automatically charged to your bank account " return _("This bill will been automatically charged to your bank account "
" with IBAN number<br><strong>%s</strong>.") % source.number " with IBAN number<br><strong>%s</strong>.") % source.number
@classmethod
def clean_data(cls, data):
data['iban'] = data['iban'].strip()
data['name'] = data['name'].strip()
IBANValidator()(data['iban'])
return data
@classmethod @classmethod
def process(cls, transactions): def process(cls, transactions):
debts = [] debts = []

View file

@ -50,6 +50,9 @@ class PaymentSource(models.Model):
def get_due_delta(self): def get_due_delta(self):
return self.method_class().due_delta return self.method_class().due_delta
def clean(self):
self.data = self.method_class().clean_data(self.data)
class TransactionQuerySet(models.QuerySet): class TransactionQuerySet(models.QuerySet):
group_by = group_by group_by = group_by

View file

@ -23,7 +23,7 @@ class SystemUserQuerySet(models.QuerySet):
class SystemUser(models.Model): class SystemUser(models.Model):
""" System users """ """ System users """
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 ./-/_ only."), help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.RegexValidator(r'^[\w.-]+$', validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')]) _("Enter a valid username."), 'invalid')])
password = models.CharField(_("password"), max_length=128) password = models.CharField(_("password"), max_length=128)

View file

@ -61,6 +61,14 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', {
}) })
WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {})
for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems():
if value is None:
WEBAPPS_TYPES.pop(webapp_type, None)
else:
WEBAPPS_TYPES[webapp_type] = value
WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5') WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5')
@ -74,51 +82,91 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
# PHP # PHP
'enabled_functions': ( 'enabled_functions': (
_("PHP - Enabled functions"), _("PHP - Enabled functions"),
r'^[\w.,-]+$' r'^[\w\.,-]+$'
),
'PHP-register_globals': (
_("PHP - Register globals"),
r'^(On|Off|on|off)$'
), ),
'PHP-allow_url_include': ( 'PHP-allow_url_include': (
_("PHP - Allow URL include"), _("PHP - Allow URL include"),
r'^(On|Off|on|off)$' r'^(On|Off|on|off)$'
), ),
'PHP-allow_url_fopen': (
_("PHP - allow_url_fopen"),
r'^(On|Off|on|off)$'
),
'PHP-auto_append_file': ( 'PHP-auto_append_file': (
_("PHP - Auto append file"), _("PHP - Auto append file"),
r'^none$' r'^[\w\.,-/]+$'
),
'PHP-auto_prepend_file': (
_("PHP - Auto prepend file"),
r'^[\w\.,-/]+$'
),
'PHP-date.timezone': (
_("PHP - date.timezone"),
r'^\w+/\w+$'
), ),
'PHP-default_socket_timeout': ( 'PHP-default_socket_timeout': (
_("PHP - Default socket timeout"), _("PHP - Default socket timeout"),
r'P^[0-9][0-9]?[0-9]?$' r'^[0-9]{1,3}$'
), ),
'PHP-display_errors': ( 'PHP-display_errors': (
_("PHP - Display errors"), _("PHP - Display errors"),
r'^(On|Off|on|off)$' r'^(On|Off|on|off)$'
), ),
'PHP-extension': (
_("PHP - Extension"),
r'^[^ ]+$'
),
'PHP-magic_quotes_gpc': ( 'PHP-magic_quotes_gpc': (
_("PHP - Magic quotes GPC"), _("PHP - Magic quotes GPC"),
r'^(On|Off|on|off)$' r'^(On|Off|on|off)$'
), ),
'PHP-magic_quotes_runtime': (
_("PHP - Magic quotes runtime"),
r'^(On|Off|on|off)$'
),
'PHP-magic_quotes_sybase': (
_("PHP - Magic quotes sybase"),
r'^(On|Off|on|off)$'
),
'PHP-max_execution_time': ( 'PHP-max_execution_time': (
_("PHP - Max execution time"), _("PHP - Max execution time"),
r'^[0-9][0-9]?[0-9]?$' r'^[0-9]{1,3}$'
), ),
'PHP-max_input_time': ( 'PHP-max_input_time': (
_("PHP - Max input time"), _("PHP - Max input time"),
r'^[0-9][0-9]?[0-9]?$' r'^[0-9]{1,3}$'
), ),
'PHP-memory_limit': ( 'PHP-memory_limit': (
_("PHP - Memory limit"), _("PHP - Memory limit"),
r'^[0-9][0-9]?[0-9]?M$' r'^[0-9]{1,3}M$'
), ),
'PHP-mysql.connect_timeout': ( 'PHP-mysql.connect_timeout': (
_("PHP - Mysql connect timeout"), _("PHP - Mysql connect timeout"),
r'^[0-9][0-9]?[0-9]?$' r'^([0-9]){1,3}$'
),
'PHP-output_buffering': (
_("PHP - output_buffering"),
r'^(On|Off|on|off)$'
),
'PHP-register_globals': (
_("PHP - Register globals"),
r'^(On|Off|on|off)$'
), ),
'PHP-post_max_size': ( 'PHP-post_max_size': (
_("PHP - Post max size"), _("PHP - Post max size"),
r'^[0-9][0-9]?M$' r'^[0-9]{1,3}M$'
),
'PHP-sendmail_path': (
_("PHP - sendmail_path"),
r'^[^ ]+$'
),
'PHP-session.bug_compat_warn': (
_("PHP - session.bug_compat_warn"),
r'^(On|Off|on|off)$'
),
'PHP-session.auto_start': (
_("PHP - session.auto_start"),
r'^(On|Off|on|off)$'
), ),
'PHP-safe_mode': ( 'PHP-safe_mode': (
_("PHP - Safe mode"), _("PHP - Safe mode"),
@ -126,32 +174,48 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
), ),
'PHP-suhosin.post.max_vars': ( 'PHP-suhosin.post.max_vars': (
_("PHP - Suhosin post max vars"), _("PHP - Suhosin post max vars"),
r'^[0-9][0-9]?[0-9]?[0-9]?$' r'^[0-9]{1,4}$'
), ),
'PHP-suhosin.request.max_vars': ( 'PHP-suhosin.request.max_vars': (
_("PHP - Suhosin request max vars"), _("PHP - Suhosin request max vars"),
r'^[0-9][0-9]?[0-9]?[0-9]?$' r'^[0-9]{1,4}$'
),
'PHP-suhosin.session.encrypt': (
_("PHP - suhosin.session.encrypt"),
r'^(On|Off|on|off)$'
), ),
'PHP-suhosin.simulation': ( 'PHP-suhosin.simulation': (
_("PHP - Suhosin simulation"), _("PHP - Suhosin simulation"),
r'^(On|Off|on|off)$' r'^(On|Off|on|off)$'
), ),
'PHP-suhosin.executor.include.whitelist': (
_("PHP - suhosin.executor.include.whitelist"),
r'^(upload|phar)$'
),
'PHP-upload_max_filesize': (
_("PHP - upload_max_filesize"),
r'^[0-9]{1,3}M$'
),
'PHP-zend_extension': (
_("PHP - zend_extension"),
r'^[^ ]+$'
),
# FCGID # FCGID
'FcgidIdleTimeout': ( 'FcgidIdleTimeout': (
_("FCGI - Idle timeout"), _("FCGI - Idle timeout"),
r'^[0-9][0-9]?[0-9]?$' r'^[0-9]{1,3}$'
), ),
'FcgidBusyTimeout': ( 'FcgidBusyTimeout': (
_("FCGI - Busy timeout"), _("FCGI - Busy timeout"),
r'^[0-9][0-9]?[0-9]?$' r'^[0-9]{1,3}$'
), ),
'FcgidConnectTimeout': ( 'FcgidConnectTimeout': (
_("FCGI - Connection timeout"), _("FCGI - Connection timeout"),
r'^[0-9][0-9]?[0-9]?$' r'^[0-9]{1,3}$'
), ),
'FcgidIOTimeout': ( 'FcgidIOTimeout': (
_("FCGI - IO timeout"), _("FCGI - IO timeout"),
r'^[0-9][0-9]?[0-9]?$' r'^[0-9]{1,3}$'
), ),
}) })

View file

@ -24,6 +24,7 @@ class Apache2Backend(ServiceController):
if site.protocol is 'https': if site.protocol is 'https':
extra_conf += self.get_ssl(site) extra_conf += self.get_ssl(site)
extra_conf += self.get_security(site) extra_conf += self.get_security(site)
extra_conf += self.get_redirect(site)
context['extra_conf'] = extra_conf context['extra_conf'] = extra_conf
apache_conf = Template(textwrap.dedent("""\ apache_conf = Template(textwrap.dedent("""\
@ -89,7 +90,7 @@ class Apache2Backend(ServiceController):
<Directory %(app_path)s> <Directory %(app_path)s>
Options +ExecCGI Options +ExecCGI
AddHandler fcgid-script .php AddHandler fcgid-script .php
FcgidWrapper %(fcgid_path)s FcgidWrapper %(fcgid_path)s\
""" % context) """ % context)
for option in content.webapp.options.filter(name__startswith='Fcgid'): for option in content.webapp.options.filter(name__startswith='Fcgid'):
fcgid += " %s %s\n" % (option.name, option.value) fcgid += " %s %s\n" % (option.name, option.value)
@ -101,10 +102,12 @@ class Apache2Backend(ServiceController):
custom_cert = site.options.filter(name='ssl') custom_cert = site.options.filter(name='ssl')
if custom_cert: if custom_cert:
cert = tuple(custom_cert[0].value.split()) cert = tuple(custom_cert[0].value.split())
# TODO separate directtives?
directives = textwrap.dedent("""\ directives = textwrap.dedent("""\
SSLEngine on SSLEngine on
SSLCertificateFile %s SSLCertificateFile %s
SSLCertificateKeyFile %s""" % cert SSLCertificateKeyFile %s\
""" % cert
) )
return directives return directives
@ -112,14 +115,24 @@ class Apache2Backend(ServiceController):
directives = '' directives = ''
for rules in site.options.filter(name='sec_rule_remove'): for rules in site.options.filter(name='sec_rule_remove'):
for rule in rules.value.split(): for rule in rules.value.split():
directives += "SecRuleRemoveById %i" % int(rule) directives += "SecRuleRemoveById %i\n" % int(rule)
for modsecurity in site.options.filter(name='sec_rule_off'): for modsecurity in site.options.filter(name='sec_rule_off'):
directives += textwrap.dedent("""\ directives += textwrap.dedent("""\
<LocationMatch %s> <LocationMatch %s>
SecRuleEngine Off SecRuleEngine Off
</LocationMatch> </LocationMatch>\
""" % modsecurity.value) """ % modsecurity.value)
if directives:
directives = '<IfModule mod_security2.c>\n%s\n</IfModule>' % directives
return directives
def get_redirect(self, site):
directives = ''
for redirect in site.options.filter(name='redirect'):
if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect.value):
directives += "RedirectMatch %s" % redirect.value
else:
directives += "Redirect %s" % redirect.value
return directives return directives
def get_protections(self, site): def get_protections(self, site):

View file

@ -17,25 +17,34 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*')
WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain') WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain')
# TODO ssl ca, ssl cert, ssl key
WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', { WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', {
# { name: ( verbose_name, validation_regex ) } # { name: ( verbose_name, validation_regex ) }
'directory_protection': ( 'directory_protection': (
_("HTTPD - Directory protection"), _("HTTPD - Directory protection"),
r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$' r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$'
), ),
'redirection': ( 'redirect': (
_("HTTPD - Redirection"), _("HTTPD - Redirection"),
r'^.*\s+.*$' r'^(permanent\s[^ ]+|[^ ]+)\s[^ ]+$'
), ),
'ssl': ( 'ssl_ca': (
_("HTTPD - SSL"), _("HTTPD - SSL CA"),
r'^.*\s+.*$' r'^[^ ]+$'
),
'ssl_cert': (
_("HTTPD - SSL cert"),
r'^[^ ]+$'
),
'ssl_key': (
_("HTTPD - SSL key"),
r'^[^ ]+$'
), ),
'sec_rule_remove': ( 'sec_rule_remove': (
_("HTTPD - SecRuleRemoveById"), _("HTTPD - SecRuleRemoveById"),
r'^[0-9,\s]+$' r'^[0-9\s]+$'
), ),
'sec_rule_off': ( 'sec_engine': (
_("HTTPD - Disable Modsecurity"), _("HTTPD - Disable Modsecurity"),
r'^[\w/_]+$' r'^[\w/_]+$'
), ),

View file

@ -155,7 +155,10 @@ function install_requirements () {
lxml==3.3.5 \ lxml==3.3.5 \
python-dateutil==2.2 \ python-dateutil==2.2 \
django-iban==0.3.0 \ django-iban==0.3.0 \
requests" requests \
phonenumbers \
django-countries \
django-localflavor"
if $testing; then if $testing; then
APT="${APT} \ APT="${APT} \

View file

@ -1,12 +1,16 @@
import re import re
import crack import crack
import localflavor
import phonenumbers
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from IPy import IP from IPy import IP
from ..utils.python import import_class
def validate_ipv4_address(value): def validate_ipv4_address(value):
msg = _("%s is not a valid IPv4 address") % value msg = _("%s is not a valid IPv4 address") % value
@ -18,7 +22,6 @@ def validate_ipv4_address(value):
raise ValidationError(msg) raise ValidationError(msg)
def validate_ipv6_address(value): def validate_ipv6_address(value):
msg = _("%s is not a valid IPv6 address") % value msg = _("%s is not a valid IPv6 address") % value
try: try:
@ -61,11 +64,12 @@ def validate_hostname(hostname):
http://stackoverflow.com/a/2532344 http://stackoverflow.com/a/2532344
""" """
if len(hostname) > 255: if len(hostname) > 255:
return False raise ValidationError(_("Too long for a hostname."))
if hostname[-1] == ".": hostname = hostname.rstrip('.')
hostname = hostname[:-1] # strip exactly one dot from the right, if present allowed = re.compile('(?!-)[A-Z\d-]{1,63}(?<!-)$', re.IGNORECASE)
allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE) for name in hostname.split('.'):
return all(allowed.match(x) for x in hostname.split(".")) if not allowed.match(name):
raise ValidationError(_("Not a valid hostname (%s).") % name)
def validate_password(value): def validate_password(value):
@ -78,3 +82,40 @@ def validate_password(value):
def validate_url_path(value): def validate_url_path(value):
if not re.match(r'^\/[/.a-zA-Z0-9-_]*$', value): if not re.match(r'^\/[/.a-zA-Z0-9-_]*$', value):
raise ValidationError(_('"%s" is not a valid URL path.') % value) raise ValidationError(_('"%s" is not a valid URL path.') % value)
def validate_vat(vat, country):
field = 'localflavor.{lower}.forms.{upper}IdentityCardNumberField'.format(
lower=country.lower(),
upper=country.upper()
)
try:
field = import_class(field)
except (ImportError, AttributeError, ValueError):
pass
else:
field().clean(vat)
def validate_zipcode(zipcode, country):
field = 'localflavor.{lower}.forms.{upper}PostalCodeField'.format(
lower=country.lower(),
upper=country.upper()
)
try:
field = import_class(field)
except (ImportError, AttributeError, ValueError):
pass
else:
field().clean(zipcode)
def validate_phone(value, country):
""" local phone number or international """
msg = _("Not a valid %s nor international phone number.") % country
try:
number = phonenumbers.parse(value, country)
except phonenumbers.phonenumberutil.NumberParseException:
raise ValidationError(msg)
if not phonenumbers.is_valid_number(number):
raise ValidationError(msg)

View file

@ -29,6 +29,10 @@ class MultiSelectField(models.CharField):
def to_python(self, value): def to_python(self, value):
if value is not None: if value is not None:
if isinstance(value, list) and value[0].startswith('('):
# Workaround unknown bug on default model values
# [u"('SUPPORT'", u" 'ADMIN'", u" 'BILLING'", u" 'TECH'", u" 'ADDS'", u" 'EMERGENCY')"]
value = list(eval(', '.join(value)))
return value if isinstance(value, list) else value.split(',') return value if isinstance(value, list) else value.split(',')
return '' return ''
@ -36,7 +40,7 @@ class MultiSelectField(models.CharField):
super(MultiSelectField, self).contribute_to_class(cls, name) super(MultiSelectField, self).contribute_to_class(cls, name)
if self.choices: if self.choices:
def func(self, field=name, choices=dict(self.choices)): def func(self, field=name, choices=dict(self.choices)):
','.join([ choices.get(value, value) for value in getattr(self, field) ]) return ','.join([ choices.get(value, value) for value in getattr(self, field) ])
setattr(cls, 'get_%s_display' % self.name, func) setattr(cls, 'get_%s_display' % self.name, func)
def validate(self, value, model_instance): def validate(self, value, model_instance):

View file

@ -21,11 +21,11 @@ def check_root(func):
return wrapped return wrapped
class _AttributeString(str): class _AttributeUnicode(unicode):
""" Simple string subclass to allow arbitrary attribute access. """ """ Simple string subclass to allow arbitrary attribute access. """
@property @property
def stdout(self): def stdout(self):
return str(self) return unicode(self)
def make_async(fd): def make_async(fd):
@ -43,7 +43,7 @@ def read_async(fd):
if e.errno != errno.EAGAIN: if e.errno != errno.EAGAIN:
raise e raise e
else: else:
return '' return u''
def run(command, display=False, error_codes=[0], silent=False, stdin=''): def run(command, display=False, error_codes=[0], silent=False, stdin=''):
@ -60,8 +60,8 @@ def run(command, display=False, error_codes=[0], silent=False, stdin=''):
make_async(p.stdout) make_async(p.stdout)
make_async(p.stderr) make_async(p.stderr)
stdout = str() stdout = unicode()
stderr = str() stderr = unicode()
# Async reading of stdout and sterr # Async reading of stdout and sterr
while True: while True:
@ -77,15 +77,15 @@ def run(command, display=False, error_codes=[0], silent=False, stdin=''):
if display and stderrPiece: if display and stderrPiece:
sys.stderr.write(stderrPiece) sys.stderr.write(stderrPiece)
stdout += stdoutPiece stdout += stdoutPiece.decode("utf8")
stderr += stderrPiece stderr += stderrPiece.decode("utf8")
returnCode = p.poll() returnCode = p.poll()
if returnCode != None: if returnCode != None:
break break
out = _AttributeString(stdout.strip()) out = _AttributeUnicode(stdout.strip())
err = _AttributeString(stderr.strip()) err = _AttributeUnicode(stderr.strip())
p.stdout.close() p.stdout.close()
p.stderr.close() p.stderr.close()

View file

@ -1,6 +1,99 @@
import re import re
import glob import glob
import sys
import errno
import fcntl
import getpass
import os
import re
import select
import subprocess
import textwrap
class _AttributeString(str):
""" Simple string subclass to allow arbitrary attribute access. """
@property
def stdout(self):
return str(self)
def make_async(fd):
""" Helper function to add the O_NONBLOCK flag to a file descriptor """
fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)
def read_async(fd):
"""
Helper function to read some data from a file descriptor, ignoring EAGAIN errors
"""
try:
return fd.read()
except IOError, e:
if e.errno != errno.EAGAIN:
raise e
else:
return ''
def run(command, display=False, error_codes=[0], silent=False, stdin=''):
""" Subprocess wrapper for running commands """
if display:
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
p = subprocess.Popen(command, shell=True, executable='/bin/bash',
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
p.stdin.write(stdin)
p.stdin.close()
make_async(p.stdout)
make_async(p.stderr)
stdout = str()
stderr = str()
# Async reading of stdout and sterr
while True:
# Wait for data to become available
select.select([p.stdout, p.stderr], [], [])
# Try reading some data from each
stdoutPiece = read_async(p.stdout)
stderrPiece = read_async(p.stderr)
if display and stdoutPiece:
sys.stdout.write(stdoutPiece)
if display and stderrPiece:
sys.stderr.write(stderrPiece)
stdout += stdoutPiece
stderr += stderrPiece
returnCode = p.poll()
if returnCode != None:
break
out = _AttributeString(stdout.strip())
err = _AttributeString(stderr.strip())
p.stdout.close()
p.stderr.close()
out.failed = False
out.return_code = returnCode
out.stderr = err
if p.returncode not in error_codes:
out.failed = True
msg = "\nrun() encountered an error (return code %s) while executing '%s'\n"
msg = msg % (p.returncode, command)
if display:
sys.stderr.write("\n\033[1;31mCommandError: %s %s\033[m\n" % (msg, err))
if not silent:
raise AttributeError("%s %s %s" % (msg, err, out))
out.succeeded = not out.failed
return out
print "from orchestra.apps.accounts.models import Account" print "from orchestra.apps.accounts.models import Account"
print "from orchestra.apps.domains.models import Domain" print "from orchestra.apps.domains.models import Domain"
@ -8,10 +101,24 @@ print "from orchestra.apps.webapps.models import WebApp"
print "from orchestra.apps.websites.models import Website, Content" print "from orchestra.apps.websites.models import Website, Content"
def print_webapp(context):
print textwrap.dedent("""\
try:
webapp = WebApp.objects.get(account=account, name='%(name)s')
except:"
webapp = WebApp.objects.create(account=account, name='%(name)s', type='%(type)s')
else:
webapp.type = '%(type)s'
webapp.save()"
Content.objects.get_or_create(website=website, webapp=webapp, path='%(path)s')
""" % context
)
for conf in glob.glob('/etc/apache2/sites-enabled/*'): for conf in glob.glob('/etc/apache2/sites-enabled/*'):
username = conf.split('/')[-1].split('.')[0] username = conf.split('/')[-1].split('.')[0]
with open(conf, 'rb') as conf: with open(conf, 'rb') as conf:
print "account = Account.objects.get(user__username='%s')" % username print "account = Account.objects.get(username='%s')" % username
for line in conf.readlines(): for line in conf.readlines():
line = line.strip() line = line.strip()
if line.startswith('<VirtualHost'): if line.startswith('<VirtualHost'):
@ -20,43 +127,64 @@ for conf in glob.glob('/etc/apache2/sites-enabled/*'):
apps = [] apps = []
if line.endswith(':443>'): if line.endswith(':443>'):
port = 443 port = 443
wrapper_root = None
webalizer = False
webappname = None
elif line.startswith("DocumentRoot"):
__, path = line.split()
webappname = path.rstrip('/').split('/')[-1]
if webappname == 'public_html':
webappname = ''
elif line.startswith("ServerName"): elif line.startswith("ServerName"):
domain = line.split()[1] __, domain = line.split()
name = domain sitename = domain
domains.append("'%s'" % domain) domains.append("'%s'" % domain)
elif line.startswith("ServerAlias"): elif line.startswith("ServerAlias"):
for domain in line.split()[1:]: for domain in line.split()[1:]:
domains.append("'%s'" % domain) domains.append("'%s'" % domain)
elif line.startswith("Alias /fcgi-bin/"): elif line.startswith("Alias /fcgi-bin/"):
fcgid = line.split('/')[-1] or line.split('/')[-2] __, __, wrapper_root = line.split()
fcgid = fcgid.split('-')[0] elif line.startswith('Action php-fcgi'):
apps.append((name, fcgid, '/')) __, __, wrapper_name = line.split()
wrapper_name = wrapper_name.split('/')[-1]
elif line.startswith("Alias /webalizer"): elif line.startswith("Alias /webalizer"):
apps.append(('webalizer', 'webalizer', '/webalizer')) webalizer = True
elif line == '</VirtualHost>': elif line == '</VirtualHost>':
if port == 443: if port == 443:
name += '-ssl' sitename += '-ssl'
print "# SITE" context = {
print "website, __ = Website.objects.get_or_create(name='%s', account=account, port=%d)" % (name, port) 'sitename': sitename,
domains = ', '.join(domains) 'port': port,
print "for domain in [%s]:" % str(domains) 'domains': ', '.join(domains),
print " try:" }
print " domain = Domain.objects.get(name=domain)" print textwrap.dedent("""\
print " except:" # SITE"
print " domain = Domain.objects.create(name=domain, account=account)" website, __ = Website.objects.get_or_create(name='%(sitename)s', account=account, port=%(port)d)
print " else:" for domain in [%(domains)s]:
print " domain.account = account" try:
print " domain.save()" domain = Domain.objects.get(name=domain)
print " website.domains.add(domain)" except:
print "" domain = Domain.objects.create(name=domain, account=account)
for name, type, path in apps: else:
print "try:" domain.account = account
print " webapp = WebApp.objects.get(account=account, name='%s')" % name domain.save()
print "except:" website.domains.add(domain)
print " webapp = WebApp.objects.create(account=account, name='%s', type='%s')" % (name, type) """ % context)
print "else:" if wrapper_root:
print " webapp.type = '%s'" % type wrapper = os.join(wrapper_root, wrapper_name)
print " webapp.save()" fcgid = run('grep "^\s*exec " %s' % wrapper).stdout
print "" type = fcgid.split()[1].split('/')[-1].split('-')[0]
print "Content.objects.get_or_create(website=website, webapp=webapp, path='%s')" % path for option in fcgid.split('-d'):
print option
print_webapp({
'name': webappname,
'path': '/',
'type': type,
})
if webalizer:
print_webapp({
'name': 'webalizer-%s' % sitename,
'path': '/webalizer',
'type': 'webalizer',
})
print '\n' print '\n'

View file

@ -1,34 +1,64 @@
#!/bin/bash #!/bin/bash
VIRTUALTABLE="/etc/postfix/virtusertable" VIRTUALTABLE=${1-"/etc/postfix/virtusertable"}
echo "from orchestra.apps.users import User" echo "from orchestra.apps.accounts.models import Account"
echo "from orchestra.apps.users.roles.mailbox import Address, Mailbox" echo "from orchestra.apps.mailboxes.models import Address, Mailbox"
echo "from orchestra.apps.domains import Domain" echo "from orchestra.apps.domains.models import Domain"
echo "main_account = Account.objects.get(id=1)"
cat "$VIRTUALTABLE"|grep -v "^\s*$"|while read line; do cat "$VIRTUALTABLE"|grep -v "^\s*$"|while read line; do
NAME=$(echo "$line" | awk {'print $1'} | cut -d'@' -f1) NAME=$(echo "$line" | awk {'print $1'} | cut -d'@' -f1)
DOMAIN=$(echo "$line" | awk {'print $1'} | cut -d'@' -f2) DOMAIN=$(echo "$line" | awk {'print $1'} | cut -d'@' -f2)
DESTINATION=$(echo "$line" | awk '{$1=""; print $0}' | sed -e 's/^ *//' -e 's/ *$//') DESTINATION=$(echo "$line" | awk '{$1=""; print $0}' | sed -e 's/^ *//' -e 's/ *$//')
echo "domain = Domain.objects.get(name='$DOMAIN')" echo "domain = Domain.objects.get(name='$DOMAIN')"
echo "mailboxes = []"
echo "account = main_account"
NEW_DESTINATION=""
for PLACE in $DESTINATION; do for PLACE in $DESTINATION; do
if [[ ! $(echo $PLACE | grep '@') ]]; then if [[ ! $(echo $PLACE | grep '@') ]]; then
echo "try:" if [[ $(grep "^${PLACE}:" /etc/shadow) ]]; then
echo " user = User.objects.get(username='$PLACE')" PASSWORD=$(grep "^${PLACE}:" /etc/shadow | cut -d':' -f2)
echo "except:" echo "if account == main_account and domain.account != main_account:"
echo " print 'User $PLACE does not exists'" echo " account = domain.account"
echo "else:" echo "else:"
echo " mailbox, __ = Mailbox.objects.get_or_create(user=user)" echo " try:"
echo " if user.account_id != 1:" echo " account = Account.objects.get(username='${PLACE}')"
echo " user.account=domain.account" echo " except:"
echo " user.save()" echo " pass"
echo "" echo "mailboxes.append(('${PLACE}', '${PASSWORD}'))"
else
NEW_DESTINATION="${NEW_DESTINATION} ${PLACE}"
fi
else
NEW_DESTINATION="${NEW_DESTINATION} ${PLACE}"
fi fi
done done
echo "address, __ = Address.objects.get_or_create(name='$NAME', domain=domain)" echo "for mailbox, password in mailboxes:"
echo "address.account=domain.account" echo " mailbox = mailbox.strip()"
echo "address.destination='$DESTINATION'" echo " try:"
echo "address.save()" echo " mailbox = Mailbox.objects.get(username=mailbox)"
echo " except:"
echo " mailbox = Mailbox(username=mailbox, password=password, account=account)"
echo " try:"
echo " mailbox.full_clean()"
echo " except:"
echo " sys.stderr.write('cleaning')"
echo " else:"
echo " mailbox.save()"
echo " else:"
echo " if mailbox.account != account:"
echo " sys.stderr.write('%s != %s' % (mailbox.account, account))"
echo " if domain.account != account:"
echo " sys.stderr.write('%s != %s' % (domain.account, account))"
echo " address = Address(name='${NAME}', domain=domain, account=account, destination='${NEW_DESTINATION}')"
echo " try:"
echo " address.full_clean()"
echo " except:"
echo " sys.stderr.write('cleaning address')"
echo " else:"
echo " address.save()"
echo " domain = None"
done done