diff --git a/INSTALL b/INSTALL index 98e146038..f59679eab 100644 --- a/INSTALL +++ b/INSTALL @@ -250,7 +250,7 @@ For *MySQL* users, define the following map files on your server (it should work password = dbname = hosts = 127.0.0.1 - query = SELECT name FROM admin_domain WHERE name='%s' AND enabled='1' + query = SELECT name FROM admin_domain WHERE name='%s' AND enabled=1 ``/etc/postfix/sql-domain-aliases.cf``:: @@ -266,7 +266,7 @@ For *MySQL* users, define the following map files on your server (it should work password = dbname = hosts = 127.0.0.1 - query = SELECT concat(t2.name, '/', t1.path) FROM admin_mailbox AS t1, admin_domain AS t2 WHERE t1.domain_id=t2.id AND t1.address='%u' AND t2.name='%d' + query = SELECT concat(t2.name, '/', t1.path) FROM admin_mailbox AS t1, admin_domain AS t2 WHERE t1.enabled=1 AND t1.domain_id=t2.id AND t1.address='%u' AND t2.enabled=1 AND t2.name='%d' ``/etc/postfix/sql-aliases.cf``:: @@ -274,7 +274,15 @@ For *MySQL* users, define the following map files on your server (it should work password = dbname = hosts = 127.0.0.1 - query = SELECT concat(t3.address, '@', t4.name) FROM admin_alias AS t1, admin_alias_mboxes AS t2, admin_mailbox AS t3, admin_domain AS t4 WHERE t1.enabled='1' AND t1.address='%u' AND t1.id=t2.alias_id AND t2.mailbox_id=t3.id AND t3.domain_id=t4.id AND t4.name='%d' + query = SELECT concat(t3.address, '@', t4.name) FROM admin_alias AS t1, admin_alias_mboxes AS t2, admin_mailbox AS t3, admin_domain AS t4 WHERE t1.enabled=1 AND t1.address='%u' AND t1.id=t2.alias_id AND t2.mailbox_id=t3.id AND t3.enabled=1 AND t3.domain_id=t4.id AND t4.enabled=1 AND t4.name='%d' + +``/etc/postfix/sql-forwards.cf``:: + + user = + password = + dbname = + hosts = 127.0.0.1 + query = SELECT t1.extmboxes FROM admin_alias AS t1, admin_domain AS t2 WHERE t1.enabled=1 AND t1.address='%u' AND t1.domain_id=t2.id AND t2.name="%d" AND t2.enabled=1 ``/etc/postfix/sql-domain-aliases-mailboxes.cf``:: @@ -301,6 +309,7 @@ Then, use the following configuration in the */etc/postfix/main.cf* file virtual_alias_domains = mysql:/etc/postfix/sql-domain-aliases.cf virtual_mailbox_maps = mysql:/etc/postfix/sql-mailboxes.cf virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf, + mysql:/etc/postfix/sql-forwards.cf, mysql:/etc/postfix/sql-domain-aliases-mailboxes.cf # Stuff after @@ -308,6 +317,51 @@ NB: Modoboa supports both maildir and mbox formats. You can specify which format to use by modifying the MAILBOX_TYPE parameter available in the admin panel. +Optional : 'catchall' aliases +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Modoboa supports 'catchall' aliases creation. In the case you would +like to use this feature, you'll need to add specific queries to +*postfix* configuration. + +First, create the following new maps: + +``/etc/postfix/sql-email2email.cf``:: + + user = + password = + dbname = + hosts = 127.0.0.1 + query = SELECT concat(t1.address, '@', t2.name) FROM admin_mailbox AS t1, admin_domain AS t2 WHERE t1.domain_id=t2.id AND t1.address='%u' AND t2.name='%d' + +``/etc/postfix/sql-catchall-aliases.cf``:: + + user = + password = + dbname = + hosts = 127.0.0.1 + query = SELECT concat(t3.address, '@', t4.name) FROM admin_alias AS t1, admin_alias_mboxes AS t2, admin_mailbox AS t3, admin_domain AS t4 WHERE t1.enabled='1' AND t1.address='*' AND t1.id=t2.alias_id AND t2.mailbox_id=t3.id AND t3.enabled='1' AND t3.domain_id=t4.id AND t4.enabled='1' AND t4.name='%d' + +``/etc/postfix/sql-catchall-forwards.cf``:: + + user = + password = + dbname = + hosts = 127.0.0.1 + query = SELECT t1.extmboxes FROM admin_alias AS t1, admin_domain AS t2 WHERE t1.enabled='1' AND t1.address='*' AND t1.domain_id=t2.id AND t2.name="%d" AND t2.enabled='1' + +Once done, edit the *main.cf* configuration file and add the new maps +to the ``virtual_alias_maps`` parameter like this:: + + virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf, + mysql:/etc/postfix/sql-forwards.cf, + mysql:/etc/postfix/sql-domain-aliases-mailboxes.cf, + mysql:/etc/postfix/sql-email2email.cf, # new + mysql:/etc/postfix/sql-catchall-aliases.cf, # new + mysql:/etc/postfix/sql-catchall-forwards.cf # new + +Reload *postfix*. + .. _dovecot-label: Dovecot @@ -529,8 +583,8 @@ images). In order to display images correctly, you need to create a $ mkdir /static/tmp -To be able to release messages, first take a look at this page: -http://www.ijs.si/software/amavisd/amavisd-new-docs.html#quar-release. It +To be able to release messages, first take a look at `this page +`_. It explains how to configure *amavisd-new* to listen somewhere for the AM.PDP protocol. This protocol is used to send release requests. diff --git a/admin/forms.py b/admin/forms.py index 2877847be..9492e1c00 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -4,7 +4,7 @@ from modoboa.admin.models import * from django.utils.translation import ugettext as _ from django.contrib.auth.models import User from modoboa.admin.templatetags.admin_extras import gender -from modoboa.lib import tables +from modoboa.lib import tables, split_mailbox class DomainForm(forms.ModelForm): class Meta: @@ -87,51 +87,70 @@ class MailboxForm(ProxyForm): return m class AliasForm(ProxyForm): - domain = forms.ModelChoiceField(queryset=Domain.objects.all(), label=_("Domain"), - required=True, - empty_label=_("Select a domain"), - help_text=_("Select a domain in the list")) + targets = forms.CharField(label=_("Target(s)"), required=False, + help_text=_("Mailbox(es) this alias will point to")) def __init__(self, *args, **kwargs): super(AliasForm, self).__init__(*args, **kwargs) self.fields['address'].widget.attrs['size'] = 14 - self.fields['mboxes'].widget.attrs['size'] = 5 # This part is a little bit tricky. As 'domain' is not a # member of Alias, we need to fill it manually. if # self._domain exists, it means a DomainAdmin is going to - # create a new alias, we know how to fill the fields. Else, - # leave them empty (super admin mode). + # create a new alias, we know how to fill the fields. if self._domain is not None: self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id) - elif len(args) < 2 or args[1]["domain"] == '': - self.fields['mboxes'].queryset = Mailbox.objects.none() # if 'instance' exists, it means we are modifying an existing - # record, we need to select the right value for - # 'domain'. Finally, if super user is doing the job, we need - # to fill 'mboxes' because it's currently empty (see upper). - if kwargs.has_key("instance") or self._domain is not None: - domain = self._domain is None and kwargs["instance"].domain or self._domain + # record, we need to select the right value for 'domain' and + # to fill the first "targets" input (because it is created by the + # Form object, not by us). + domain = None + if self._domain: + domain = self._domain + if kwargs.has_key("instance"): + if self._domain is None: + domain = kwargs["instance"].domain + self.fields["targets"].initial = kwargs["instance"].first_target() + if domain is not None: self.fields["domain"].initial = domain - self.fields['mboxes'].queryset = Mailbox.objects.filter(domain=domain) - self.fields.keyOrder = ['domain', 'address', 'mboxes', 'enabled'] + self.fields.keyOrder = ['address', 'domain', 'enabled', 'targets'] class Meta: model = Alias - fields = ('address', 'mboxes', 'enabled') + fields = ('address', 'domain', 'enabled') def clean_address(self): if self.cleaned_data["address"].find('@') != -1: return self.cleaned_data["address"].rsplit("@", 1)[0] return self.cleaned_data["address"] + def set_targets(self, user, values): + self.ext_targets = [] + self.int_targets = [] + for addr in values: + if addr == "": + continue + local_part, domain = split_mailbox(addr) + if domain is None: + raise AdminError("%s %s" % (_("Invalid mailbox"), addr)) + try: + mb = Mailbox.objects.get(address=local_part, domain__name=domain) + except Mailbox.DoesNotExist: + self.ext_targets += [addr] + else: + if not user.is_superuser: + usermb = Mailbox.objects.get(user=user.id) + if usermb.domain.id != mb.domain.id: + raise AdminError("%s %s" % (_("Permission denied on"), addr)) + self.int_targets += [mb] + def save(self, force_insert=False, force_update=False, commit=True): a = super(AliasForm, self).save(commit=False) if commit: - domain = self.cleaned_data["domain"].name - a.save() + a.save(self.int_targets, self.ext_targets) + self.save_m2m() return a class SuperAdminForm(forms.Form): diff --git a/admin/migrations/0009_auto__add_field_alias_extmboxes.py b/admin/migrations/0009_auto__add_field_alias_extmboxes.py new file mode 100644 index 000000000..9f10660a7 --- /dev/null +++ b/admin/migrations/0009_auto__add_field_alias_extmboxes.py @@ -0,0 +1,103 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'Alias.extmboxes' + db.add_column('admin_alias', 'extmboxes', self.gf('django.db.models.fields.TextField')(default='', blank=True), keep_default=False) + db.add_column('admin_alias', 'domain', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['admin.Domain'], default=-1)) + + + def backwards(self, orm): + + # Deleting field 'Alias.extmboxes' + db.delete_column('admin_alias', 'extmboxes') + db.delete_column('admin_alias', 'domain') + + + models = { + 'admin.alias': { + 'Meta': {'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'object_name': 'Domain'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'gid': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}), + 'uid': ('django.db.models.fields.IntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['admin'] diff --git a/admin/migrations/0010_fill_alias_domain.py b/admin/migrations/0010_fill_alias_domain.py new file mode 100644 index 000000000..bad4d465d --- /dev/null +++ b/admin/migrations/0010_fill_alias_domain.py @@ -0,0 +1,101 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + for alias in orm.Alias.objects.all(): + print alias.address + alias.domain = alias.mboxes.all()[0].domain + alias.save() + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'admin.alias': { + 'Meta': {'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'object_name': 'Domain'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'gid': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}), + 'uid': ('django.db.models.fields.IntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['admin'] diff --git a/admin/models.py b/admin/models.py index 3ae9dd99e..4d59008e0 100644 --- a/admin/models.py +++ b/admin/models.py @@ -215,9 +215,11 @@ class Mailbox(models.Model): class Alias(models.Model): address = models.CharField(_('address'), max_length=254, - help_text=_("The alias address (without the domain part)")) + help_text=_("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.")) + domain = models.ForeignKey(Domain) mboxes = models.ManyToManyField(Mailbox, verbose_name=_('mailboxes'), help_text=_("The mailboxes this alias points to")) + extmboxes = models.TextField(blank=True) enabled = models.BooleanField(_('enabled'), help_text=_("Check to activate this alias")) @@ -226,10 +228,50 @@ class Alias(models.Model): ("view_aliases", "View aliases"), ) - def __domain(self): - return self.mboxes.all()[0].domain + def save(self, int_targets, ext_targets, *args, **kwargs): + if len(ext_targets): + self.extmboxes = ",".join(ext_targets) + super(Alias, self).save(*args, **kwargs) + curmboxes = self.mboxes.all() + for t in int_targets: + if not t in curmboxes: + self.mboxes.add(t) + for t in curmboxes: + if not t in int_targets: + self.mboxes.remove(t) - domain = property(__domain) + def first_target(self): + """Return the first target of this alias + + It is taken from internal addresses, or external addresses. + """ + if len(self.mboxes.all()): + return self.mboxes.all()[0] + if len(self.extmboxes): + return self.extmboxes.split(",")[0] + return None + + def get_targets(self): + """Return a list containing targeted mailboxes + + Internal and external addresses are mixed into a single list. + """ + result = [] + if len(self.mboxes.all()): + for mb in self.mboxes.all()[1:]: + result += [mb.full_address] + if len(self.extmboxes): + result += self.extmboxes.split(",") + else: + result = self.extmboxes.split(",")[1:] + return result + + def repr_extmboxes(self): + """Return external targets as a string + + A ready to print representation of external targets. + """ + return "
".join(self.extmboxes.split(",")) class Extension(models.Model): name = models.CharField(max_length=150) diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index 3e3ab29ce..3df3a625a 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -206,7 +206,7 @@ def mbalias_actions(user, alias): "img" : static_url("pics/edit.png"), "title" : _("Edit mailbox alias"), "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:300,y:300}}"} + "rel" : "{handler:'iframe',size:{x:320,y:300}}"} ] return render_actions(actions) diff --git a/admin/urls.py b/admin/urls.py index 81d8822cf..922d614f3 100644 --- a/admin/urls.py +++ b/admin/urls.py @@ -16,6 +16,7 @@ urlpatterns = patterns('modoboa.admin.views', (r'^mailboxes/new/$', 'newmailbox'), (r'^mailboxes/edit/(?P\d+)/$', 'editmailbox'), (r'^mailboxes/delete/$', 'delmailbox'), + (r'^mailboxes/search/$', 'mailboxes_search'), (r'^mbaliases/$', 'mbaliases'), (r'^mbaliases/new/$', 'newmbalias'), diff --git a/admin/views.py b/admin/views.py index 8d398e48b..ab30b955c 100644 --- a/admin/views.py +++ b/admin/views.py @@ -6,6 +6,7 @@ from django.utils.http import urlquote from django.core.urlresolvers import reverse from django.contrib.auth.decorators \ import login_required, permission_required, user_passes_test +from django.db.models import Q from django.contrib.auth.models import User, Group from django.contrib import messages from django.db import transaction, IntegrityError @@ -14,7 +15,8 @@ from modoboa import admin, userprefs from models import * from admin.permissions import * from modoboa.lib import crypt_password -from modoboa.lib import _render, ajax_response, getctx, events, parameters +from modoboa.lib import _render, ajax_response, ajax_simple_response, \ + getctx, events, parameters, split_mailbox from modoboa.lib.models import Parameter import copy @@ -60,7 +62,7 @@ def domains(request): domains = Domain.objects.all() for dom in domains: - dom.mbalias_counter = len(Alias.objects.filter(mboxes__domain__pk=dom.id)) + dom.mbalias_counter = len(Alias.objects.filter(domain=dom.id)) deloptions = {"keepdir" : _("Do not delete domain directory")} return render_domains_page(request, "domains", domains=domains, @@ -266,6 +268,22 @@ def mailboxes_raw(request, dom_id=None): "target" : target }) +@login_required +@good_domain +@permission_required("admin.view_mailboxes") +def mailboxes_search(request): + if request.method != "POST": + return + local_part, domain = split_mailbox(request.POST["search"]) + query = Q(address__startswith=local_part) + if domain is not None and domain != "": + query &= Q(domain__name__startswith=domain) + if request.GET.has_key("domid"): + query = Q(domain=request.GET["domid"]) & query + mboxes = Mailbox.objects.filter(query) + result = map(lambda mb: mb.full_address, mboxes) + return ajax_simple_response(result) + @login_required @good_domain @permission_required("admin.add_mailbox") @@ -361,13 +379,50 @@ def delmailbox(request): @permission_required("admin.view_aliases") def mbaliases(request): if request.GET.has_key("domid"): - aliases = Alias.objects.filter(mboxes__domain__pk=request.GET["domid"]).distinct() + aliases = Alias.objects.filter(domain=request.GET["domid"]) elif request.GET.has_key("mbid"): aliases = Alias.objects.filter(mboxes__id=request.GET["mbid"]).distinct() else: aliases = Alias.objects.all() + if not request.user.is_superuser: + usermb = Mailbox.objects.get(user=request.user.id) + for al in aliases: + al.ui_disabled = False + for mb in al.mboxes.all(): + if mb.domain.id != usermb.domain.id: + al.ui_disabled = True + break return render_domains_page(request, "mbaliases", aliases=aliases) +def _validate_mbalias(request, form, successmsg, tplname, commonctx): + """Mailbox alias validation + + Common function shared between creation and modification actions. + """ + error = None + if form.is_valid(): + try: + if not request.POST.has_key("targets"): + raise AdminError(_("No target defined")) + form.set_targets(request.user, request.POST.getlist("targets")) + try: + alias = form.save() + except IntegrityError: + raise AdminError(_("Alias with this name already exists")) + + messages.info(request, successmsg, fail_silently=True) + return ajax_response(request, url=reverse(admin.views.mbaliases)) + except AdminError, e: + error = str(e) + + if request.POST.has_key("targets"): + targets = request.POST.getlist("targets") + commonctx["targets"] = targets[:-1] + + commonctx["form"] = form + commonctx["error"] = error + return ajax_response(request, status="ko", template=tplname, **commonctx) + @login_required @good_domain @permission_required("admin.add_alias") @@ -378,24 +433,10 @@ def newmbalias(request, tplname="admin/mbaliasform.html"): "formid" : "mbaliasform"} if request.method == "POST": form = AliasForm(request.user, request.POST) - error = None - if form.is_valid(): - try: - alias = form.save() - except IntegrityError: - error = _("Alias with this name already exists") - else: - form.save_m2m() - messages.info(request, _("Mailbox alias created"), fail_silently=True) - return ajax_response(request, url=reverse(admin.views.mbaliases)) - - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) + return _validate_mbalias(request, form, _("Mailbox alias created"), + tplname, commonctx) form = AliasForm(request.user) - if request.GET.has_key("mbid"): - form.fields["mboxes"].initial = request.GET["mbid"] commonctx["form"] = form commonctx["noerrors"] = True return _render(request, tplname, commonctx) @@ -411,23 +452,12 @@ def editmbalias(request, alias_id, tplname="admin/mbaliasform.html"): "formid" : "mbaliasform"} if request.method == "POST": form = AliasForm(request.user, request.POST, instance=alias) - error = None - if form.is_valid(): - try: - alias = form.save() - except IntegrityError: - error = _("Alias with this name already exists") - else: - form.save_m2m() - messages.info(request, _("Mailbox alias modified"), - fail_silently=True) - return ajax_response(request, url=reverse(admin.views.mbaliases)) - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) + return _validate_mbalias(request, form, _("Mailbox alias modified"), + tplname, commonctx) form = AliasForm(request.user, instance=alias) commonctx["form"] = form + commonctx["targets"] = alias.get_targets() return _render(request, tplname, commonctx) @login_required diff --git a/extensions/sievefilters/lib.py b/extensions/sievefilters/lib.py index e91acd14b..05928185f 100644 --- a/extensions/sievefilters/lib.py +++ b/extensions/sievefilters/lib.py @@ -2,7 +2,8 @@ from django.utils.translation import ugettext as _ from managesieve import ManageSieveClient, Error -from modoboa.lib import ConnectionsManager, ConnectionError, parameters +from modoboa.lib import parameters +from modoboa.lib.connections import ConnectionsManager, ConnectionError class SieveClientError(Exception): pass diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index f0893d0fe..28a5eab52 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -15,7 +15,8 @@ import lxml from django.utils.translation import ugettext as _ from django.conf import settings from modoboa.lib import decode, u2u_decode, tables, \ - imap_utf7, static_url, ConnectionsManager, parameters + imap_utf7, static_url, parameters +from modoboa.lib.connections import ConnectionsManager from modoboa.lib.email_listing import MBconnector, EmailListing, Email class WebmailError(Exception): diff --git a/lib/__init__.py b/lib/__init__.py index 9e58335ae..3b1b42e81 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -12,7 +12,6 @@ from django.template.loader import render_to_string from django.utils.translation import ugettext as _ from django.utils import simplejson from modoboa.lib import parameters -from modoboa.auth.lib import decrypt def exec_cmd(cmd, **kwargs): import subprocess @@ -33,7 +32,7 @@ def _render(request, tpl, user_context): def _render_to_string(request, tpl, user_context): """Custom rendering function - Same a mrender_render. + Same as _render. """ return render_to_string(tpl, user_context, context_instance=RequestContext(request)) @@ -109,6 +108,15 @@ def ajax_response(request, status="ok", respmsg=None, jsonctx["norefresh"] = norefresh return HttpResponse(simplejson.dumps(jsonctx), mimetype="application/json") +def ajax_simple_response(content): + """Simple AJAX response + + No extra formatting is done. The content is passed directly to simplejon. + + :param content: the response's content (list, dict, string) + """ + return HttpResponse(simplejson.dumps(content), mimetype="application/json") + def decode(s, encodings=('utf8', 'latin1', 'windows-1252', 'ascii'), charset=None): if charset is not None: try: @@ -177,6 +185,14 @@ def crypt_password(password): return password def split_mailbox(mailbox): + """Try to split a mailbox in two parts + + :return: a 2-uple (local part, domain) + """ + try: + mailbox.index("@") + except ValueError: + return mailbox, None parts = mailbox.split('@') if len(parts) == 2: address = parts[0] @@ -185,33 +201,3 @@ def split_mailbox(mailbox): domain = parts[-1] address = "@".join(parts[:-1]) return (address, domain) - -class ConnectionsManager(type): - """Singleton pattern implementation - - This class is specialized in connection management. - """ - def __init__(cls, name, bases, dict): - super(ConnectionsManager, cls).__init__(name, bases, dict) - cls.instances = {} - - def __call__(cls, **kwargs): - key = None - if kwargs.has_key("user"): - key = kwargs["user"] - else: - return None - if not cls.instances.has_key(key): - cls.instances[key] = None - if kwargs.has_key("password"): - kwargs["password"] = decrypt(kwargs["password"]) - - if cls.instances[key] is None: - cls.instances[key] = \ - super(ConnectionsManager, cls).__call__(**kwargs) - else: - cls.instances[key].refresh(key, kwargs["password"]) - return cls.instances[key] - -class ConnectionError(Exception): - pass diff --git a/lib/connections.py b/lib/connections.py new file mode 100644 index 000000000..0cff589e0 --- /dev/null +++ b/lib/connections.py @@ -0,0 +1,31 @@ +from modoboa.auth.lib import decrypt + +class ConnectionsManager(type): + """Singleton pattern implementation + + This class is specialized in connection management. + """ + def __init__(cls, name, bases, dict): + super(ConnectionsManager, cls).__init__(name, bases, dict) + cls.instances = {} + + def __call__(cls, **kwargs): + key = None + if kwargs.has_key("user"): + key = kwargs["user"] + else: + return None + if not cls.instances.has_key(key): + cls.instances[key] = None + if kwargs.has_key("password"): + kwargs["password"] = decrypt(kwargs["password"]) + + if cls.instances[key] is None: + cls.instances[key] = \ + super(ConnectionsManager, cls).__call__(**kwargs) + else: + cls.instances[key].refresh(key, kwargs["password"]) + return cls.instances[key] + +class ConnectionError(Exception): + pass diff --git a/lib/migrations/0002_move_secret_key.py b/lib/migrations/0002_move_secret_key.py index d6151d6a7..77ee5bdd0 100644 --- a/lib/migrations/0002_move_secret_key.py +++ b/lib/migrations/0002_move_secret_key.py @@ -8,9 +8,13 @@ class Migration(DataMigration): def forwards(self, orm): "Write your forwards methods here." - p = orm.Parameter.objects.get(name="webmail.SECRET_KEY") - p.name = "auth.SECRET_KEY" - p.save() + try: + p = orm.Parameter.objects.get(name="webmail.SECRET_KEY") + except orm.Parameter.DoesNotExist: + pass + else: + p.name = "auth.SECRET_KEY" + p.save() def backwards(self, orm): "Write your backwards methods here." diff --git a/static/css/Autocompleter.css b/static/css/Autocompleter.css new file mode 100644 index 000000000..99a3c727e --- /dev/null +++ b/static/css/Autocompleter.css @@ -0,0 +1,50 @@ +ul.autocompleter-choices +{ + position: absolute; + margin: 0; + padding: 0; + list-style: none; + border: 1px solid #7c7c7c; + border-left-color: #c3c3c3; + border-right-color: #c3c3c3; + border-bottom-color: #ddd; + background-color: #fff; + text-align: left; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; + z-index: 50; + background-color: #fff; +} + +ul.autocompleter-choices li +{ + position: relative; + margin: -2px 0 0 0; + padding: 0.2em 1.5em 0.2em 1em; + display: block; + float: none !important; + cursor: pointer; + font-weight: normal; + white-space: nowrap; + font-size: 1em; + line-height: 1.5em; +} + +ul.autocompleter-choices li.autocompleter-selected +{ + background-color: #444; + color: #fff; +} + +ul.autocompleter-choices span.autocompleter-queried +{ + display: inline; + float: none; + font-weight: bold; + margin: 0; + padding: 0; +} + +ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried +{ + color: #9FCFFF; +} \ No newline at end of file diff --git a/static/js/Autocompleter.Local.js b/static/js/Autocompleter.Local.js new file mode 100644 index 000000000..d7594b8da --- /dev/null +++ b/static/js/Autocompleter.Local.js @@ -0,0 +1,31 @@ +/** + * Autocompleter.Local + * + * http://digitarald.de/project/autocompleter/ + * + * @version 1.1.2 + * + * @license MIT-style license + * @author Harald Kirschner + * @copyright Author + */ + +Autocompleter.Local = new Class({ + + Extends: Autocompleter, + + options: { + minLength: 0, + delay: 200 + }, + + initialize: function(element, tokens, options) { + this.parent(element, options); + this.tokens = tokens; + }, + + query: function() { + this.update(this.filter()); + } + +}); \ No newline at end of file diff --git a/static/js/Autocompleter.Request.js b/static/js/Autocompleter.Request.js new file mode 100644 index 000000000..ffb7ddf2e --- /dev/null +++ b/static/js/Autocompleter.Request.js @@ -0,0 +1,109 @@ +/** + * Autocompleter.Request + * + * http://digitarald.de/project/autocompleter/ + * + * @version 1.1.2 + * + * @license MIT-style license + * @author Harald Kirschner + * @copyright Author + */ + +Autocompleter.Request = new Class({ + + Extends: Autocompleter, + + options: {/* + indicator: null, + indicatorClass: null, + onRequest: $empty, + onComplete: $empty,*/ + postData: {}, + ajaxOptions: {}, + postVar: 'value' + + }, + + query: function(){ + var data = $unlink(this.options.postData) || {}; + data[this.options.postVar] = this.queryValue; + var indicator = $(this.options.indicator); + if (indicator) indicator.setStyle('display', ''); + var cls = this.options.indicatorClass; + if (cls) this.element.addClass(cls); + this.fireEvent('onRequest', [this.element, this.request, data, this.queryValue]); + this.request.send({'data': data}); + }, + + /** + * queryResponse - abstract + * + * Inherated classes have to extend this function and use this.parent() + */ + queryResponse: function() { + var indicator = $(this.options.indicator); + if (indicator) indicator.setStyle('display', 'none'); + var cls = this.options.indicatorClass; + if (cls) this.element.removeClass(cls); + return this.fireEvent('onComplete', [this.element, this.request]); + } + +}); + +Autocompleter.Request.JSON = new Class({ + + Extends: Autocompleter.Request, + + initialize: function(el, url, options) { + this.parent(el, options); + this.request = new Request.JSON($merge({ + 'url': url, + 'link': 'cancel' + }, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this)); + }, + + queryResponse: function(response) { + this.parent(); + this.update(response); + } + +}); + +Autocompleter.Request.HTML = new Class({ + + Extends: Autocompleter.Request, + + initialize: function(el, url, options) { + this.parent(el, options); + this.request = new Request.HTML($merge({ + 'url': url, + 'link': 'cancel', + 'update': this.choices + }, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this)); + }, + + queryResponse: function(tree, elements) { + this.parent(); + if (!elements || !elements.length) { + this.hideChoices(); + } else { + this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice || function(choice) { + var value = choice.innerHTML; + choice.inputValue = value; + this.addChoiceEvents(choice.set('html', this.markQueryValue(value))); + }, this); + this.showChoices(); + } + + } + +}); + +/* compatibility */ + +Autocompleter.Ajax = { + Base: Autocompleter.Request, + Json: Autocompleter.Request.JSON, + Xhtml: Autocompleter.Request.HTML +}; diff --git a/static/js/Autocompleter.js b/static/js/Autocompleter.js new file mode 100644 index 000000000..d9d78cbc9 --- /dev/null +++ b/static/js/Autocompleter.js @@ -0,0 +1,442 @@ +/** + * Autocompleter + * + * http://digitarald.de/project/autocompleter/ + * + * @version 1.1.2 + * + * @license MIT-style license + * @author Harald Kirschner + * @copyright Author + */ + +var Autocompleter = new Class({ + + Implements: [Options, Events], + + options: {/* + onOver: $empty, + onSelect: $empty, + onSelection: $empty, + onShow: $empty, + onHide: $empty, + onBlur: $empty, + onFocus: $empty,*/ + minLength: 1, + markQuery: true, + width: 'inherit', + maxChoices: 10, + injectChoice: null, + customChoices: null, + emptyChoices: null, + visibleChoices: true, + className: 'autocompleter-choices', + zIndex: 42, + delay: 400, + observerOptions: {}, + fxOptions: {}, + + autoSubmit: false, + overflow: false, + overflowMargin: 25, + selectFirst: false, + filter: null, + filterCase: false, + filterSubset: false, + forceSelect: false, + selectMode: true, + choicesMatch: null, + + multiple: false, + separator: ', ', + separatorSplit: /\s*[,;]\s*/, + autoTrim: false, + allowDupes: false, + + cache: true, + relative: false + }, + + initialize: function(element, options) { + this.element = $(element); + this.setOptions(options); + this.build(); + this.observer = new Observer(this.element, this.prefetch.bind(this), $merge({ + 'delay': this.options.delay + }, this.options.observerOptions)); + this.queryValue = null; + if (this.options.filter) this.filter = this.options.filter.bind(this); + var mode = this.options.selectMode; + this.typeAhead = (mode == 'type-ahead'); + this.selectMode = (mode === true) ? 'selection' : mode; + this.cached = []; + }, + + /** + * build - Initialize DOM + * + * Builds the html structure for choices and appends the events to the element. + * Override this function to modify the html generation. + */ + build: function() { + if ($(this.options.customChoices)) { + this.choices = this.options.customChoices; + } else { + this.choices = new Element('ul', { + 'class': this.options.className, + 'styles': { + 'zIndex': this.options.zIndex + } + }).inject(document.body); + this.relative = false; + if (this.options.relative) { + this.choices.inject(this.element, 'after'); + this.relative = this.element.getOffsetParent(); + } + this.fix = new OverlayFix(this.choices); + } + if (!this.options.separator.test(this.options.separatorSplit)) { + this.options.separatorSplit = this.options.separator; + } + this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, $merge({ + 'property': 'opacity', + 'link': 'cancel', + 'duration': 200 + }, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0); + this.element.setProperty('autocomplete', 'off') + .addEvent((Browser.Engine.trident || Browser.Engine.webkit) ? 'keydown' : 'keypress', this.onCommand.bind(this)) + .addEvent('click', this.onCommand.bind(this, [false])) + .addEvent('focus', this.toggleFocus.create({bind: this, arguments: true, delay: 100})) + .addEvent('blur', this.toggleFocus.create({bind: this, arguments: false, delay: 100})); + }, + + destroy: function() { + if (this.fix) this.fix.destroy(); + this.choices = this.selected = this.choices.destroy(); + }, + + toggleFocus: function(state) { + this.focussed = state; + if (!state) this.hideChoices(true); + this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]); + }, + + onCommand: function(e) { + if (!e && this.focussed) return this.prefetch(); + if (e && e.key && !e.shift) { + switch (e.key) { + case 'enter': + if (this.element.value != this.opted) return true; + if (this.selected && this.visible) { + this.choiceSelect(this.selected); + return !!(this.options.autoSubmit); + } + break; + case 'up': case 'down': + if (!this.prefetch() && this.queryValue !== null) { + var up = (e.key == 'up'); + this.choiceOver((this.selected || this.choices)[ + (this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst') + ](this.options.choicesMatch), true); + } + return false; + case 'esc': case 'tab': + this.hideChoices(true); + break; + } + } + return true; + }, + + setSelection: function(finish) { + var input = this.selected.inputValue, value = input; + var start = this.queryValue.length, end = input.length; + if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0; + if (this.options.multiple) { + var split = this.options.separatorSplit; + value = this.element.value; + start += this.queryIndex; + end += this.queryIndex; + var old = value.substr(this.queryIndex).split(split, 1)[0]; + value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length); + if (finish) { + var tokens = value.split(this.options.separatorSplit).filter(function(entry) { + return this.test(entry); + }, /[^\s,]+/); + if (!this.options.allowDupes) tokens = [].combine(tokens); + var sep = this.options.separator; + value = tokens.join(sep) + sep; + end = value.length; + } + } + this.observer.setValue(value); + this.opted = value; + if (finish || this.selectMode == 'pick') start = end; + this.element.selectRange(start, end); + this.fireEvent('onSelection', [this.element, this.selected, value, input]); + }, + + showChoices: function() { + var match = this.options.choicesMatch, first = this.choices.getFirst(match); + this.selected = this.selectedValue = null; + if (this.fix) { + var pos = this.element.getCoordinates(this.relative), width = this.options.width || 'auto'; + this.choices.setStyles({ + 'left': pos.left, + 'top': pos.bottom, + 'width': (width === true || width == 'inherit') ? pos.width : width + }); + } + if (!first) return; + if (!this.visible) { + this.visible = true; + this.choices.setStyle('display', ''); + if (this.fx) this.fx.start(1); + this.fireEvent('onShow', [this.element, this.choices]); + } + if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead); + var items = this.choices.getChildren(match), max = this.options.maxChoices; + var styles = {'overflowY': 'hidden', 'height': ''}; + this.overflown = false; + if (items.length > max) { + var item = items[max - 1]; + styles.overflowY = 'scroll'; + styles.height = item.getCoordinates(this.choices).bottom; + this.overflown = true; + }; + this.choices.setStyles(styles); + this.fix.show(); + if (this.options.visibleChoices) { + var scroll = document.getScroll(), + size = document.getSize(), + coords = this.choices.getCoordinates(); + if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x; + if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y; + window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top)); + } + }, + + hideChoices: function(clear) { + if (clear) { + var value = this.element.value; + if (this.options.forceSelect) value = this.opted; + if (this.options.autoTrim) { + value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator); + } + this.observer.setValue(value); + } + if (!this.visible) return; + this.visible = false; + if (this.selected) this.selected.removeClass('autocompleter-selected'); + this.observer.clear(); + var hide = function(){ + this.choices.setStyle('display', 'none'); + this.fix.hide(); + }.bind(this); + if (this.fx) this.fx.start(0).chain(hide); + else hide(); + this.fireEvent('onHide', [this.element, this.choices]); + }, + + prefetch: function() { + var value = this.element.value, query = value; + if (this.options.multiple) { + var split = this.options.separatorSplit; + var values = value.split(split); + var index = this.element.getSelectedRange().start; + var toIndex = value.substr(0, index).split(split); + var last = toIndex.length - 1; + index -= toIndex[last].length; + query = values[last]; + } + if (query.length < this.options.minLength) { + this.hideChoices(); + } else { + if (query === this.queryValue || (this.visible && query == this.selectedValue)) { + if (this.visible) return false; + this.showChoices(); + } else { + this.queryValue = query; + this.queryIndex = index; + if (!this.fetchCached()) this.query(); + } + } + return true; + }, + + fetchCached: function() { + return false; + if (!this.options.cache + || !this.cached + || !this.cached.length + || this.cached.length >= this.options.maxChoices + || this.queryValue) return false; + this.update(this.filter(this.cached)); + return true; + }, + + update: function(tokens) { + this.choices.empty(); + this.cached = tokens; + var type = tokens && $type(tokens); + if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())) { + (this.options.emptyChoices || this.hideChoices).call(this); + } else { + if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices; + tokens.each(this.options.injectChoice || function(token){ + var choice = new Element('li', {'html': this.markQueryValue(token)}); + choice.inputValue = token; + this.addChoiceEvents(choice).inject(this.choices); + }, this); + this.showChoices(); + } + }, + + choiceOver: function(choice, selection) { + if (!choice || choice == this.selected) return; + if (this.selected) this.selected.removeClass('autocompleter-selected'); + this.selected = choice.addClass('autocompleter-selected'); + this.fireEvent('onSelect', [this.element, this.selected, selection]); + if (!this.selectMode) this.opted = this.element.value; + if (!selection) return; + this.selectedValue = this.selected.inputValue; + if (this.overflown) { + var coords = this.selected.getCoordinates(this.choices), margin = this.options.overflowMargin, + top = this.choices.scrollTop, height = this.choices.offsetHeight, bottom = top + height; + if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0); + else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom); + } + if (this.selectMode) this.setSelection(); + }, + + choiceSelect: function(choice) { + if (choice) this.choiceOver(choice); + this.setSelection(true); + this.queryValue = false; + this.hideChoices(); + }, + + filter: function(tokens) { + return (tokens || this.tokens).filter(function(token) { + return this.test(token); + }, new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i')); + }, + + /** + * markQueryValue + * + * Marks the queried word in the given string with * + * Call this i.e. from your custom parseChoices, same for addChoiceEvents + * + * @param {String} Text + * @return {String} Text + */ + markQueryValue: function(str) { + return (!this.options.markQuery || !this.queryValue) ? str + : str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '$1'); + }, + + /** + * addChoiceEvents + * + * Appends the needed event handlers for a choice-entry to the given element. + * + * @param {Element} Choice entry + * @return {Element} Choice entry + */ + addChoiceEvents: function(el) { + return el.addEvents({ + 'mouseover': this.choiceOver.bind(this, [el]), + 'click': this.choiceSelect.bind(this, [el]) + }); + } +}); + +var OverlayFix = new Class({ + + initialize: function(el) { + if (Browser.Engine.trident) { + this.element = $(el); + this.relative = this.element.getOffsetParent(); + this.fix = new Element('iframe', { + 'frameborder': '0', + 'scrolling': 'no', + 'src': 'javascript:false;', + 'styles': { + 'position': 'absolute', + 'border': 'none', + 'display': 'none', + 'filter': 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)' + } + }).inject(this.element, 'after'); + } + }, + + show: function() { + if (this.fix) { + var coords = this.element.getCoordinates(this.relative); + delete coords.right; + delete coords.bottom; + this.fix.setStyles($extend(coords, { + 'display': '', + 'zIndex': (this.element.getStyle('zIndex') || 1) - 1 + })); + } + return this; + }, + + hide: function() { + if (this.fix) this.fix.setStyle('display', 'none'); + return this; + }, + + destroy: function() { + if (this.fix) this.fix = this.fix.destroy(); + } + +}); + +Element.implement({ + + getSelectedRange: function() { + if (!Browser.Engine.trident) return {start: this.selectionStart, end: this.selectionEnd}; + var pos = {start: 0, end: 0}; + var range = this.getDocument().selection.createRange(); + if (!range || range.parentElement() != this) return pos; + var dup = range.duplicate(); + if (this.type == 'text') { + pos.start = 0 - dup.moveStart('character', -100000); + pos.end = pos.start + range.text.length; + } else { + var value = this.value; + var offset = value.length - value.match(/[\n\r]*$/)[0].length; + dup.moveToElementText(this); + dup.setEndPoint('StartToEnd', range); + pos.end = offset - dup.text.length; + dup.setEndPoint('StartToStart', range); + pos.start = offset - dup.text.length; + } + return pos; + }, + + selectRange: function(start, end) { + if (Browser.Engine.trident) { + var diff = this.value.substr(start, end - start).replace(/\r/g, '').length; + start = this.value.substr(0, start).replace(/\r/g, '').length; + var range = this.createTextRange(); + range.collapse(true); + range.moveEnd('character', start + diff); + range.moveStart('character', start); + range.select(); + } else { + this.focus(); + this.setSelectionRange(start, end); + } + return this; + } + +}); + +/* compatibility */ + +Autocompleter.Base = Autocompleter; \ No newline at end of file diff --git a/static/js/Observer.js b/static/js/Observer.js new file mode 100644 index 000000000..7724816b7 --- /dev/null +++ b/static/js/Observer.js @@ -0,0 +1,69 @@ +/** + * Observer - Observe formelements for changes + * + * - Additional code from clientside.cnet.com + * + * @version 1.1 + * + * @license MIT-style license + * @author Harald Kirschner + * @copyright Author + */ +var Observer = new Class({ + + Implements: [Options, Events], + + options: { + periodical: false, + delay: 1000 + }, + + initialize: function(el, onFired, options){ + this.element = $(el) || $$(el); + this.addEvent('onFired', onFired); + this.setOptions(options); + this.bound = this.changed.bind(this); + this.resume(); + }, + + changed: function() { + var value = this.element.get('value'); + if ($equals(this.value, value)) return; + this.clear(); + this.value = value; + this.timeout = this.onFired.delay(this.options.delay, this); + }, + + setValue: function(value) { + this.value = value; + this.element.set('value', value); + return this.clear(); + }, + + onFired: function() { + this.fireEvent('onFired', [this.value, this.element]); + }, + + clear: function() { + $clear(this.timeout || null); + return this; + }, + + pause: function(){ + if (this.timer) $clear(this.timer); + else this.element.removeEvent('keyup', this.bound); + return this.clear(); + }, + + resume: function(){ + this.value = this.element.get('value'); + if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this); + else this.element.addEvent('keyup', this.bound); + return this; + } + +}); + +var $equals = function(obj1, obj2) { + return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2)); +}; \ No newline at end of file diff --git a/static/js/SqueezeBox.js b/static/js/SqueezeBox.js index 218088994..b386bca6c 100644 --- a/static/js/SqueezeBox.js +++ b/static/js/SqueezeBox.js @@ -118,15 +118,14 @@ var SqueezeBox = { return !SqueezeBox.fromElement(this, options); }); }, - + open: function(subject, options) { this.initialize(); if (this.element != null) this.trash(); this.element = $(subject) || false; - + this.setOptions(Object.merge(this.presets, options || {})); - if (this.element && this.options.parse) { var obj = this.element.getProperty(this.options.parse); if (obj && (obj = JSON.decode(obj, this.options.parseSecure))) this.setOptions(obj); @@ -134,7 +133,7 @@ var SqueezeBox = { this.url = ((this.element) ? (this.element.get('href')) : subject) || this.options.url || ''; this.assignOptions(); - + var handler = handler || this.options.handler; if (handler) return this.setContent(handler, this.parsers[handler].call(this, true)); var ret = false; @@ -147,7 +146,7 @@ var SqueezeBox = { return false; }, this); }, - + fromElement: function(from, options) { return this.open(from, options); }, diff --git a/static/js/confirmation.js b/static/js/confirmation.js index b4c9d491b..04d260904 100644 --- a/static/js/confirmation.js +++ b/static/js/confirmation.js @@ -25,7 +25,7 @@ var Confirmation = new Class({ id: "dlg-iconbox", html: "Warning" }).inject(content); - + this.textdiv = new Element("div", { id: "dlg-textbox", html: "" + question + "" @@ -42,19 +42,19 @@ var Confirmation = new Class({ .addEvent("click", this.cancel_handler) .inject(buttonsdiv); - new Element("input", {type: "hidden", name: "result", id: "result", value: "cancel"}) + new Element("input", {type: "hidden", name: "result", + id: "result", value: "cancel"}) .inject(content); - SqueezeBox.initialize({ - size: {x: 350, y: 140}, + SqueezeBox.open(content, { + size: {x: 350, y: 140}, handler: "adopt", - onClose: this.close_handler + onClose: this.close_handler.bind(content) }); - SqueezeBox.open(content); }, close_handler: function(content) { - var result = $("confirmbox").getElement("input[name=result]"); + var result = this.getElement("input[name=result]"); if (result.get("value") != "ok") { return; } diff --git a/static/js/domains.js b/static/js/domains.js index e7d7a59ae..e4758ac29 100644 --- a/static/js/domains.js +++ b/static/js/domains.js @@ -12,18 +12,6 @@ window.addEvent('domready', function() { }); } - if ($$("a[name=deleteDom]")) - $$("a[name=deleteDom]").addEvent('click', function(event) { - if (!confirm(gettext("Delete this domain?"))) - event.stop(); - }); - - if ($$("a[name=deleteAlias]")) - $$("a[name=deleteAlias]").addEvent('click', function(event) { - if (!confirm(gettext("Delete this alias?"))) - event.stop(); - }); - $(document.body).addEvent('click', function(event) { var target = event.target; if (target.get('tag') == 'a') { @@ -56,14 +44,3 @@ window.addEvent('domready', function() { selectable: true }); }); - -loadPage = function(url, destination, callback) { - destination = (destination == null) ? "page" : destination; - - if (callback) - new Request({url: url, method: "get", onSuccess: callback}).send(); - else - new Request({url: url, method: "get", onSuccess: function(data) { - $(destination).set('html', data); - }}).send(); -}; diff --git a/static/js/dynamic_textinput.js b/static/js/dynamic_textinput.js new file mode 100644 index 000000000..d8891d067 --- /dev/null +++ b/static/js/dynamic_textinput.js @@ -0,0 +1,77 @@ + +var DynamicTextInput = new Class({ + Implements: [Options], + + options: { + + }, + + initialize: function(targets, options) { + if (!$defined(targets) || !targets.length) { + return; + } + for (var cpt = 0; cpt < targets.length; cpt++) { + var target = targets[cpt]; + var btn; + + if (cpt == 0) { + btn = new Element("img", { + src: parent.static_url("pics/add.png") + }); + btn.addEvent("click", this.newinput.bind(this)); + } else { + var btnid = "btn_" + cpt; + btn = new Element("img", { + src: parent.static_url("pics/remove.png"), + id: btnid + }); + btn.addEvent("click", this.removeinput.bind(this)); + } + btn.setStyles({ + "float": "right", + "margin-top": "2px" + }); + btn.inject(target, "after"); + } + this.nextid = targets.length; + this.target = targets[0]; + this.setOptions(options); + }, + + newinput: function(event) { + if (this.target.get("value") == "") { + return; + } + var coords = this.target.getCoordinates(); + var div = new Element("div").addClass("row"); + var label = new Element("label").inject(div); + var ninput = new Element("input", { + type: "text", + id: this.target.get("id") + "_" + this.nextid, + name: this.target.get("name"), + value: this.target.get("value") + }).inject(div); + var delbtn = new Element("img", { + id: "delbtn_" + this.nextid, + src: parent.static_url("pics/remove.png") + }); + + this.nextid += 1; + delbtn.setStyles({ + "float" : "right", + "margin-top": "2px" + }); + div.inject(this.target.getParent(), "after"); + delbtn.inject(ninput, "after"); + delbtn.addEvent("click", this.removeinput.bind(this)); + this.target.set("value", ""); + }, + + removeinput: function(event) { + var id = (event.target.get("id").split("_"))[1]; + var input = $("id_" + this.target.get("name") + "_" + id); + + input.destroy(); + event.target.destroy(); + } +}); \ No newline at end of file diff --git a/static/js/form.js b/static/js/form.js index 9afb11770..7f7e62dac 100644 --- a/static/js/form.js +++ b/static/js/form.js @@ -1,10 +1,4 @@ var error_callbacks = { - mbaliasform: function() { - if ($("id_domain").value == 0) { - $("id_mboxes").set("html", ""); - } - mbaliasform_init(); - }, permform: function() { if ($defined($("id_domain"))) { $("id_domain").options[0].setProperty("selected", "selected"); diff --git a/templates/admin/adminform.html b/templates/admin/adminform.html index 52c3ccdc5..2ca1b1d34 100644 --- a/templates/admin/adminform.html +++ b/templates/admin/adminform.html @@ -7,6 +7,7 @@ {% if error %}
{{ error }}
{% endif %}
{% include "common/genericform.html" %} + {% block extrafields %}{% endblock %}
diff --git a/templates/admin/domains_header.html b/templates/admin/domains_header.html index e61c31289..a78f0c003 100644 --- a/templates/admin/domains_header.html +++ b/templates/admin/domains_header.html @@ -12,11 +12,10 @@ window.addEvent("domready", function() { var sizes = $("objects").get("rel").split(" "); evt.stop(); - SqueezeBox.initialize({ - size: {x: sizes[0], y: sizes[1]}, + SqueezeBox.open(rooturl + type + "/new/", { + size: {x: parseInt(sizes[0]), y: parseInt(sizes[1])}, handler: 'iframe' }); - SqueezeBox.open(rooturl + type + "/new/"); }); $$("a[name=remove]").addEvent("click", function(evt) { @@ -28,11 +27,19 @@ window.addEvent("domready", function() { var selection = ""; objtable._selectedRows.each(function(item) { + var options = JSON.decode(item.get("rel")); + + if ($defined(options.ui_disabled) && options.ui_disabled == "True") { + return; + } if (selection != "") { selection += ","; } selection += item.get("id"); }); + if (selection == "") { + return; + } new Confirmation(rooturl + type + "/delete/", "selection=" + selection, gettext("Remove this selection?"), { {% if deloptions %}checkboxes: { {% join deloptions %} }{% endif %} diff --git a/templates/admin/mbaliases.html b/templates/admin/mbaliases.html index f0176347f..94ea9407b 100644 --- a/templates/admin/mbaliases.html +++ b/templates/admin/mbaliases.html @@ -2,27 +2,31 @@ {% load i18n admin_extras %} {% block pagetitle %}{% trans "Mailbox aliases" %}{% endblock %} {% block subcontent %} -
+
{% if not aliases %}
{% trans "No mailbox alias defined." %}
{% else %} - - + + {% for alias in aliases %} - + - + {% endfor %}
{% trans "Address" %}{% trans "Mailbox" %}{% trans "Address" %}{% trans "Target(s)" %} {% trans "Enabled" %} {% trans "Actions" %}
{{ alias.address }}@{{ alias.domain }} {% for mb in alias.mboxes.all %} {{ mb.full_address }}
{% endfor %} + {{ alias.repr_extmboxes|safe }}
{{ alias.enabled|yesno }}{% mbalias_actions user alias %} + {% if alias.ui_disabled %}--{% else %}{% mbalias_actions user alias %}{% endif %} +
diff --git a/templates/admin/mbaliasform.html b/templates/admin/mbaliasform.html index c41a2cb45..f31802848 100644 --- a/templates/admin/mbaliasform.html +++ b/templates/admin/mbaliasform.html @@ -1,24 +1,34 @@ {% extends "admin/adminform.html" %} +{% load i18n %} {% block html_head %} {{ block.super }} + + + + + {% endblock %} +{% block extrafields %} +{% for t in targets %} +
+ + +
+{% endfor %} +{% endblock %} diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/pfxadmin_migrate/__init__.py b/tools/pfxadmin_migrate/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/tools/pfxadmin_migrate/migrate.py b/tools/pfxadmin_migrate/migrate.py new file mode 100755 index 000000000..1226a002d --- /dev/null +++ b/tools/pfxadmin_migrate/migrate.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +Automatic postfixadmin to Modoboa migration +=========================================== + +This script provides an easy way to migrate an existing postfixadmin +database to a Modoboa one. + +As the two products do not share the same schema, some informations +will be lost on the new database. Here is the list: + * Description, transport and backup MX for domains + * Dates (creation/modification) + * Logs + * Fetchmail table + +Mailboxes organization on the filesystem will be changed. PostfixAdmin +uses the following layout:: + + /domain.tld/user@domain.tld/ + +Whereas Modoboa uses the following:: + + /domain.tld/user/ + +To rename directories, you'll need the appropriate permissions. + +Domain administrators that manage more than one domain will not be +completly restored. They will only be administrator of the domain that +corresponds to their username. + +Vacation messages will not be migrated by this script. Users will have +to define new messages by using Modoboa's autoreply plugin. (if +activated) + +One last point about passwords. PostfixAdmin uses different hash +algorithms to store passwords so this script does not modify +them. They are copied "as is" is the new database. In order to use +them, you'll have to adapt the ``PASSWORD_SCHEME`` parameter in the +admin. panel. Currently, it appears that the ``crypt`` scheme is +compatible with PostfixAdmin ``md5crypt`` algorithm... + +""" +from django.conf import settings +from django.db.utils import ConnectionDoesNotExist +from django.db.models import Q +from django.contrib.auth.models import Group +import modoboa.admin.models as md_models +import models as pf_models +from modoboa.lib import split_mailbox, exec_cmd + +def migrate_domain_aliases(domain, options): + print "\tMigrating domain aliases" + old_domain_aliases = pf_models.AliasDomain.objects.using(options._from).filter(target_domain=domain.name) + for old_da in old_domain_aliases: + new_da = md_models.DomainAlias() + new_da.name = old_da.alias_domain + new_da.target = old_da.target_domain + new_da.enabled = new_da.active + new_da.save(using=options.to) + +def migrate_mailbox_aliases(domain, options): + print "\tMigrating mailbox aliases" + old_aliases = pf_models.Alias.objects.using(options._from).filter(domain=domain.name) + for old_al in old_aliases: + if old_al.address == old_al.goto: + continue + new_al = md_models.Alias() + local_part, tmp = split_mailbox(old_al.address) + if local_part is None or not len(local_part): + if tmp is None or not len(tmp): + print """Warning: skipping alias %s (cannot retrieve local part). +You will need to recreate it manually. +""" % old_al.address + continue + new_al.address = "*" + else: + new_al.address = local_part + new_al.domain = domain + new_al.enabled = old_al.active + extmboxes = [] + intmboxes = [] + for goto in old_al.goto.split(","): + try: + mb = md_models.Mailbox.objects.using(options.to).get(user__username=goto) + except md_models.Mailbox.DoesNotExist: + extmboxes += [goto] + else: + intmboxes += [mb] + new_al.save(intmboxes, extmboxes, using=options.to) + +def migrate_mailboxes(domain, options): + print "\tMigrating mailboxes" + old_mboxes = pf_models.Mailbox.objects.using(options._from).filter(domain=domain.name) + for old_mb in old_mboxes: + new_mb = md_models.Mailbox() + new_mb.name = old_mb.name + new_mb.address = old_mb.local_part + new_mb.password = old_mb.password + new_mb.domain = domain + if old_mb.quota: + new_mb.quota = old_mb.quota / 1024000 + + new_mb.path = "%s/" % old_mb.local_part + if options.rename_dirs: + oldpath = os.path.join(options.mboxes_path, domain.name, old_mb.maildir) + newpath = os.path.join(options.mboxes_path, domain.name, new_mb.path) + code, output = exec_cmd("mv %s %s" % (oldpath, newpath)) + if code: + print "Error: cannot rename mailbox directory\n%s" % output + sys.exit(1) + + new_mb.save(using=options.to) + + +def migrate_domain(old_dom, options): + print "Migrating domain %s" % old_dom.domain + newdom = md_models.Domain() + newdom.name = old_dom.domain + newdom.enabled = old_dom.active + newdom.quota = old_dom.maxquota + newdom.save(using=options.to) + migrate_mailboxes(newdom, options) + migrate_mailbox_aliases(newdom, options) + migrate_domain_aliases(newdom, options) + +def migrate_admins(options): + print "Migrating administrators" + + dagroup = Group.objects.using(options.to).get(name="DomainAdmins") + for old_admin in pf_models.Admin.objects.using(options._from).all(): + local_part, domname = split_mailbox(old_admin.username) + try: + query = Q(username=old_admin.username) & \ + (Q(domain="ALL") | Q(domain=domname)) + creds = pf_models.DomainAdmins.objects.using(options._from).get(query) + except pf_models.DomainAdmins.DoesNotExist: + print "Warning: skipping useless admin %s" % (old_admin.username) + continue + try: + mb = md_models.Mailbox.objects.using(options.to).get(user__username=old_admin.username) + except md_models.Mailbox.DoesNotExist: + try: + domain = md_models.Domain.objects.using(options.to).get(name=domname) + except md_models.Domain.DoesNotExist: + print "Warning: skipping domain admin %s, domain not found" \ + % old_admin.username + continue + mb = md_models.Mailbox() + mb.address = local_part + mb.domain = domain + mb.name = local_part + mb.enabled = old_admin.active + mb.password = old_admin.password + mb.save(using=options.to) + + if creds.domain == "ALL": + mb.user.is_superuser = True + else: + mb.user.groups.add(dagroup) + mb.save(using=options.to) + +def do_migration(options): + pf_domains = pf_models.Domain.objects.using(options._from).all() + for olddom in pf_domains: + if olddom.domain == "ALL": + continue + migrate_domain(olddom, options) + migrate_admins(options) + +if __name__ == "__main__": + from optparse import OptionParser + + parser = OptionParser() + parser.add_option("-f", "--from", dest="_from", default="pfxadmin", + help="Name of postfixadmin db connection declared in settings.py") + parser.add_option("-t", "--to", default="modoboa", + help="Name of the Modoboa db connection declared in settings.py") + parser.add_option("-r", "--rename-dirs", action="store_true", + help="Rename mailbox directories (default is no)") + parser.add_option("-p", "--mboxes-path", default=None, + help="Path where directories are stored on the filesystem (used only if --rename-dirs)") + + options, args = parser.parse_args() + + if options.rename_dirs and options.mboxes_path is None: + print "Error: you must provide the --mboxes-path option" + sys.exit(1) + + try: + do_migration(options) + except ConnectionDoesNotExist, e: + print e + + + + diff --git a/tools/pfxadmin_migrate/models.py b/tools/pfxadmin_migrate/models.py new file mode 100755 index 000000000..b45ac935b --- /dev/null +++ b/tools/pfxadmin_migrate/models.py @@ -0,0 +1,157 @@ +# This is an auto-generated Django model module. +# You'll have to do the following manually to clean this up: +# * Rearrange models' order +# * Make sure each model has one field with primary_key=True +# Feel free to rename the models, but don't rename db_table values or field names. +# +# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]' +# into your database. + +from django.db import models + +class Admin(models.Model): + username = models.CharField(max_length=765, primary_key=True) + password = models.CharField(max_length=765) + created = models.DateTimeField() + modified = models.DateTimeField() + active = models.IntegerField() + class Meta: + db_table = u'admin' + app_label = 'pfxadmin_migrate' + +class Alias(models.Model): + address = models.CharField(max_length=765, primary_key=True) + goto = models.TextField() + domain = models.CharField(max_length=765) + created = models.DateTimeField() + modified = models.DateTimeField() + active = models.IntegerField() + class Meta: + db_table = u'alias' + app_label = 'pfxadmin_migrate' + +class AliasDomain(models.Model): + alias_domain = models.CharField(max_length=765, primary_key=True) + target_domain = models.CharField(max_length=765) + created = models.DateTimeField() + modified = models.DateTimeField() + active = models.IntegerField() + class Meta: + db_table = u'alias_domain' + app_label = 'pfxadmin_migrate' + +class Config(models.Model): + id = models.IntegerField(primary_key=True) + name = models.CharField(unique=True, max_length=60) + value = models.CharField(max_length=60) + class Meta: + db_table = u'config' + app_label = 'pfxadmin_migrate' + +class Domain(models.Model): + domain = models.CharField(max_length=765, primary_key=True) + description = models.CharField(max_length=765) + aliases = models.IntegerField() + mailboxes = models.IntegerField() + maxquota = models.BigIntegerField() + quota = models.BigIntegerField() + transport = models.CharField(max_length=765) + backupmx = models.IntegerField() + created = models.DateTimeField() + modified = models.DateTimeField() + active = models.IntegerField() + class Meta: + db_table = u'domain' + app_label = 'pfxadmin_migrate' + +class DomainAdmins(models.Model): + username = models.CharField(max_length=765) + domain = models.CharField(max_length=765) + created = models.DateTimeField(primary_key=True) + active = models.IntegerField() + class Meta: + db_table = u'domain_admins' + app_label = 'pfxadmin_migrate' + +class Fetchmail(models.Model): + id = models.IntegerField(primary_key=True) + mailbox = models.CharField(max_length=765) + src_server = models.CharField(max_length=765) + src_auth = models.CharField(max_length=33, blank=True) + src_user = models.CharField(max_length=765) + src_password = models.CharField(max_length=765) + src_folder = models.CharField(max_length=765) + poll_time = models.IntegerField() + fetchall = models.IntegerField() + keep = models.IntegerField() + protocol = models.CharField(max_length=12, blank=True) + usessl = models.IntegerField() + extra_options = models.TextField(blank=True) + returned_text = models.TextField(blank=True) + mda = models.CharField(max_length=765) + date = models.DateTimeField() + class Meta: + db_table = u'fetchmail' + app_label = 'pfxadmin_migrate' + +class Log(models.Model): + timestamp = models.DateTimeField() + username = models.CharField(max_length=765) + domain = models.CharField(max_length=765) + action = models.CharField(max_length=765) + data = models.TextField() + class Meta: + db_table = u'log' + app_label = 'pfxadmin_migrate' + +class Mailbox(models.Model): + username = models.CharField(max_length=765, primary_key=True) + password = models.CharField(max_length=765) + name = models.CharField(max_length=765) + maildir = models.CharField(max_length=765) + quota = models.BigIntegerField() + local_part = models.CharField(max_length=765) + domain = models.CharField(max_length=765) + created = models.DateTimeField() + modified = models.DateTimeField() + active = models.IntegerField() + class Meta: + db_table = u'mailbox' + app_label = 'pfxadmin_migrate' + +class Quota(models.Model): + username = models.CharField(max_length=765, primary_key=True) + path = models.CharField(max_length=300, primary_key=True) + current = models.BigIntegerField(null=True, blank=True) + class Meta: + db_table = u'quota' + app_label = 'pfxadmin_migrate' + +class Quota2(models.Model): + username = models.CharField(max_length=300, primary_key=True) + bytes = models.BigIntegerField() + messages = models.IntegerField() + class Meta: + db_table = u'quota2' + app_label = 'pfxadmin_migrate' + +class Vacation(models.Model): + email = models.CharField(max_length=765) + subject = models.CharField(max_length=765) + body = models.TextField() + cache = models.TextField() + domain = models.CharField(max_length=765) + created = models.DateTimeField() + active = models.IntegerField() + class Meta: + db_table = u'vacation' + app_label = 'pfxadmin_migrate' + +class VacationNotification(models.Model): + on_vacation = models.ForeignKey(Vacation, db_column='on_vacation') + notified = models.CharField(max_length=765, primary_key=True) + notified_at = models.DateTimeField() + class Meta: + db_table = u'vacation_notification' + app_label = 'pfxadmin_migrate' +