diff --git a/doc/conf.py b/doc/conf.py index 44f06ec80..88bf64a2e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,9 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = [ + 'sphinx.ext.intersphinx', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] @@ -41,16 +43,16 @@ master_doc = 'index' # General information about the project. project = u'Modoboa' -copyright = u'2014, Antoine Nguyen' +copyright = u'2015, Antoine Nguyen' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.2' +version = '1.3' # The full version, including alpha/beta/rc tags. -release = '1.2.1' +release = '1.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -219,3 +221,7 @@ man_pages = [ ('index', 'modoboa', u'Modoboa Documentation', [u'Antoine Nguyen'], 1) ] + +intersphinx_mapping = { + 'amavis': ('http://modoboa-amavis.readthedocs.org/en/latest/', None) +} diff --git a/doc/extending/events.rst b/doc/extending/events.rst index 8b780d4a6..5d4964a2a 100644 --- a/doc/extending/events.rst +++ b/doc/extending/events.rst @@ -619,14 +619,16 @@ name, the model name and the permission name. Example:: GetExtraRoles ============= -Let extensions define new administrative roles. +Let extensions define new administrative roles (will be used to create +or modify an account). *Callback prototype*:: - def callback(user): pass + def callback(user, account): pass * ``user`` is a ``User`` instance corresponding to the currently logged in user +* ``account`` is the account being modified (None on creation) Callbacks listening to this event must return a list of 2uple (two strings) which respect the following format: ``(value, label)``. @@ -922,11 +924,12 @@ account. *Callback prototype*:: - def callback(account, role): pass + def callback(user, role, account): pass * ``user`` is the ``User`` instance corresponding to the currently logged in user * ``role`` is the role ``user`` tries to set +* ``account`` is the account being modified (None on creation) Must return a list containing ``True`` or ``False`` to indicate if this user can is allowed to set ``role``. diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 611cf84ca..bdd09dbee 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -93,37 +93,31 @@ Deployment `modoboa-admin.py`, a command line tool, lets you deploy a *ready-to-use* Modoboa site using only one instruction:: - $ modoboa-admin.py deploy modoboa_example --collectstatic [--with-amavis] [--dburl database-url] [--amavis_dburl database-url] - -Just answer the few questions and you're done. You can now go to the -:ref:`first_use` section. + $ modoboa-admin.py deploy modoboa_example --collectstatic [--dburl default:database-url] [--extensions extensions] .. note:: - The `--with-amavis` option must be set only if you intend to use - the :ref:`amavis_frontend`. - -In case you need a **silent installation** (e.g. if you're using -Salt-Stack, Ansible or whatever), it's possible to supply the database -credentials as commandline arguments. - -You can see the complete option list by running the following command:: - - $ modoboa-admin.py help deploy + By default, only the core application of Modoboa is installed. To + install extensions, use the ``--extensions`` option which accepts a + list of extension names as argument (``--extensions ext1 ext2 ...``). + If you want to use install all extensions, just use the ``all`` + shortcut like this ``--extensions all``. .. note:: - `--dburl database-url` for the modoboa database credentials - `--amavis_dburl database-url` for the amavis database credentials + You can specify more than one database connection using the + ``--dburl`` option. Multiple connections are differentiated by a + prefix. The primary connection must use the ``default:`` prefix (as + shown in the example above). For the `amavis extension + `_ extension, use the + ``amavis:`` prefix like this ``--dburl amavis:``. - Your database-url should meet the following syntax: - ``scheme://[user:pass@][host:port]/dbname`` + Your database url should meet the following syntax + ``scheme://[user:pass@][host:port]/dbname`` **OR** + ``sqlite:////full/path/to/your/database/file.sqlite``. - **or** + Available schemes are: - ``sqlite:////full/path/to/your/database/file.sqlite`` - - Available `schemes` are: * postgres * postgresql * postgis @@ -131,6 +125,16 @@ You can see the complete option list by running the following command:: * mysql2 * sqlite +The command will ask you a few questions, answer them and you're +done. You can now go to the :ref:`first_use` section. + +In case you need a **silent installation** (e.g. if you're using +Salt-Stack, Ansible or whatever), it's possible to supply the database +credentials as commandline arguments. + +You can see the complete option list by running the following command:: + + $ modoboa-admin.py help deploy .. note:: diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index 230ad644a..f773c1224 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -33,9 +33,8 @@ your instance was first deployed:: If you customized your configuration file (:file:`settings.py`) with non-standard settings, you'll have to re-apply them. -Then, run ``modoboa-admin.py deploy``:: - - $ modoboa-admin.py deploy --collectstatic [--with-amavis] [--dburl database-url] [--amavis_dburl database-url] [--domain hostname] [--lang lang] [--timezone timezone] +Finally, run the ``deploy`` comamand. Make sure to consult the +:ref:`deployment` section to know more about the available options. If you prefer the manual way, check if :ref:`specific_upgrade_instructions` are required according to the @@ -50,6 +49,64 @@ you did choose. See :ref:`webservers` for more details. Specific upgrade instructions ***************************** +1.3.0 +===== + +This release does not bring awesome new features but it is a necessary +bridge to the future of Modoboa. All extensions now have their own git +repository and the deploy process has been updated to reflect this +change. + +Another important update is the use of Django 1.7. Besides its new +features, the migration system has been reworked and is now more +robust than before. + +Before we begin with the procedure, here is a table showing old +extension names and their new name: + ++----------------------------------------+--------------------------+--------------------------+ +|Old name |New package name |New module name | ++========================================+==========================+==========================+ +|modoboa.extensions.admin |modoboa-admin |modoboa_admin | ++----------------------------------------+--------------------------+--------------------------+ +|modoboa.extensions.limits |modoboa-admin-limits |modoboa_admin_limits | ++----------------------------------------+--------------------------+--------------------------+ +|modoboa.extensions.postfix_autoreply |modoboa-postfix-autoreply |modoboa_postfix_autoreply | ++----------------------------------------+--------------------------+--------------------------+ +|modoboa.extensions.postfix_relay_domains|modoboa-admin-relaydomains|modoboa_admin_relaydomains| ++----------------------------------------+--------------------------+--------------------------+ +|modoboa.extensions.radicale |modoboa-radicale |modoboa_radicale | ++----------------------------------------+--------------------------+--------------------------+ +|modoboa.extensions.sievefilters |modoboa-sievefilters |modoboa_sievefilters | ++----------------------------------------+--------------------------+--------------------------+ +|modoboa.extensions.stats |modoboa-stats |modoboa_stats | ++----------------------------------------+--------------------------+--------------------------+ +|modoboa.extensions.webmail |modoboa-webmail |modoboa_webmail | ++----------------------------------------+--------------------------+--------------------------+ + +Here are the required steps: + +#. Install the extensions using pip (look at the second column in the table above):: + + $ pip install + +#. Remove ``south`` from ``INSTALLED_APPS`` + +#. Rename old extension names inside ``MODOBOA_APPS`` (look at the third column in the table above) + +#. Remove ``modoboa.lib.middleware.ExtControlMiddleware`` from ``MIDDLEWARE_CLASSES`` + +#. Change ``DATABASE_ROUTERS`` to:: + + DATABASE_ROUTERS = ["modoboa_amavis.dbrouter.AmavisRouter"] + +#. Run the following commands:: + + $ cd + $ python manage.py migrate + $ python manage.py load_initial_data + $ python manage.py collectstatic + 1.2.0 ===== @@ -105,7 +162,7 @@ If you plan to use the Radicale extension: A new feature allows administrators and users to manually train SpamAssassin in order to customize its behaviour. -Check :ref:`sa_manual_learning` to know more about this feature. +Check :ref:`amavis:sa_manual_learning` to know more about this feature. 1.1.6: Few bugfixes =================== diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index d14f413bd..be670a535 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -422,9 +422,9 @@ be used by Postfix to lookup into Modoboa tables. To automaticaly generate the requested map files and store them in a directory, run the following command:: - $ modoboa-admin.py postfix_maps --dbtype mapfiles + $ modoboa-admin.py postfix_maps --dbtype --extensions all -:file:`mapfiles` is the directory where the files will be +```` is the directory where the files will be stored. Answer the few questions and you're done. .. _postfix_config: diff --git a/doc/plugins/amavis.rst b/doc/plugins/amavis.rst index fb5b043c9..268448917 100644 --- a/doc/plugins/amavis.rst +++ b/doc/plugins/amavis.rst @@ -4,304 +4,4 @@ Amavisd-new frontend #################### -This plugin provides a simple management frontend for `amavisd-new -`_. The supported features are: - -* SQL quarantine management : available to administrators or users, - possibility to delete or release messages -* Per domain customization (using policies): specify how amavisd-new - will handle traffic - -.. note:: - - The per-domain policies feature only works for new - installations. Currently, you can't use modoboa with an existing - database (ie. with data in ``users`` and ``policies`` tables). - -.. note:: - - This plugin requires amavisd-new version **2.7.0** or higher. If - you're planning to use the :ref:`selfservice`, you'll need version - **2.8.0**. - -Quick Amavis setup -================== - -By default, amavis doesn't use a database. To configure this -behaviour, you first need to create a dedicated database. This step is a -bit manual since no *ready-to-use* SQL schema is provided by -amavis. The information is located inside README files, one for `MySQL -`_ and one for `PostgreSQL -`_. - -Then, you must tell amavis to use this database for lookups and -quarantined messages storing. Here is a working configuration sample:: - - @lookup_sql_dsn = - (['DBI::database=;host=;port=', '', ']']); - - @storage_sql_dsn = - (['DBI::database=;host=;port=', '', ']']); - - # PostgreSQL users NEED this parameter! - # MySQL users only need this parameter is email addresses are stored - # using the VARBINARY type. - $sql_allow_8bit_address = 1; - - $virus_quarantine_method = 'sql:'; - $spam_quarantine_method = 'sql:'; - $banned_files_quarantine_method = 'sql:'; - $bad_header_quarantine_method = 'sql:'; - - $virus_quarantine_to = 'virus-quarantine'; - $banned_quarantine_to = 'banned-quarantine'; - $bad_header_quarantine_to = 'bad-header-quarantine'; - $spam_quarantine_to = 'spam-quarantine'; - -Replace values between ``<>`` by yours. To know how to configure -amavis to allow quarantined messages release, read this :ref:`section -`. - -.. note:: - - Amavis configuration allows for separate lookup and storage - databases but Modoboa doesn't support it yet. - -Connect Modoboa and Amavis -========================== - -You must tell to Modoboa where it can find the amavis -database. Inside :file:`settings.py`, add a new connection to the -``DATABASES`` variable like this:: - - DATABASES = { - # Stuff before - # - "amavis": { - "ENGINE" : "", - "HOST" : "", - "NAME" : "", - "USER" : "", - "PASSWORD" : "" - } - } - -Replace values between ``<>`` with yours. - -Cleanup -------- - -Storing quarantined messages to a database can quickly become a -perfomance killer. Modoboa provides a simple script to periodically -purge the quarantine database. To use it, add the following line -inside root's crontab:: - - 0 0 * * * /manage.py qcleanup - # - # Or like this if you use a virtual environment: - # 0 0 * * * /manage.py qcleanup - -Replace ``modoboa_site`` with the path of your Modoboa instance. - -By default, messages older than 14 days are automatically purged. You -can modify this value by changing the ``MAX_MESSAGES_AGE`` parameter -in the online panel. - -.. _amavis_release: - -Release messages -================ - -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 requests. - -Below is an example of a working configuration:: - - $interface_policy{'SOCK'} = 'AM.PDP-SOCK'; - $interface_policy{'9998'} = 'AM.PDP-INET'; - - $policy_bank{'AM.PDP-SOCK'} = { - protocol => 'AM.PDP', - auth_required_release => 0, - }; - $policy_bank{'AM.PDP-INET'} = { - protocol => 'AM.PDP', - inet_acl => [qw( 127.0.0.1 [::1] )], - }; - -Don't forget to update the ``inet_acl`` list if you plan to release from -the network. - -Once amavisd-new is configured, just tell Modoboa where it can find -the *release server* by modifying the following parameters in the -online panel: - -+--------------------+--------------------+------------------------+ -|Name |Description |Default value | -+====================+====================+========================+ -|Amavis connection |Mode used to access |unix | -|mode |the PDP server | | -+--------------------+--------------------+------------------------+ -|PDP server address |PDP server address |localhost | -| |(if inet mode) | | -+--------------------+--------------------+------------------------+ -|PDP server port |PDP server port (if | | -| |inet mode) 9998 | | -+--------------------+--------------------+------------------------+ -|PDP server socket |Path to the PDP |/var/amavis/amavisd.sock| -| |server socket (if | | -| |unix mode) | | -+--------------------+--------------------+------------------------+ - -Deferred release ----------------- - -By default, simple users are not allowed to release messages -themselves. They are only allowed to send release requests to -administrators. - -As administrators are not always available or logged into Modoboa, a -notification tool is available. It sends reminder e-mails to every -administrators or domain administrators. To use it, add the following -example line to root's crontab:: - - 0 12 * * * /manage.py amnotify --baseurl='' - # - # Or like this if you use a virtual environment: - # 0 12 * * * /manage.py amnotify --baseurl='' - -You are free to change the frequency. - -.. note:: - - If you want to let users release their messages alone (not - recommended), go to the admin panel. - -The following parameters are available to let you customize this -feature: - -+--------------------+--------------------+------------------------+ -|Name |Description |Default value | -+====================+====================+========================+ -|Check requests |Interval between two|30 | -|interval |release requests | | -| |checks | | -+--------------------+--------------------+------------------------+ -|Allow direct release|Allow users to |no | -| |directly release | | -| |their messages | | -+--------------------+--------------------+------------------------+ -|Notifications sender|The e-mail address |notification@modoboa.org| -| |used to send | | -| |notitications | | -+--------------------+--------------------+------------------------+ - -.. _selfservice: - -Self-service mode -================= - -The *self-service* mode lets users act on quarantined messages without -beeing authenticated. They can: - -* View messages -* Remove messages -* Release messages (or send release requests) - -To access a specific message, they only need the following information: - -* Message's unique identifier -* Message's secret identifier - -This information is controlled by *amavis*, which is in charge of -notifying users when new messages are put into quarantine. Each -notification (one per message) must embark a direct link containing -the required identifiers. - -To activate this feature, go the administration panel and set the -**Enable self-service mode** parameter to yes. - -The last step is to customize the notification messages amavis -sends. The most important is to embark a direct link. Take a look at -the `README.customize `_ file to -learn what you're allowed to do. - -Here is a link example:: - - http:///quarantine/%i/?rcpt=%R&secret_id=[:secret_id] - -.. _sa_manual_learning: - -Manual SpamAssassin learning -============================ - -It is possible to manually train `SpamAssassin -`_ using the quarantine's content. By -train, we mean: - -* Mark message(s) as spam (false negative(s)) - -* Mark message(s) as non-spam (false positive(s)) - -This feature is available to all users (from super administrators to -simple users) but not enabled by default. - -SpamAssassin configuration --------------------------- - -For better performance and to enable the per-user level, SpamAssassin -must store bayes information into a SQL database. - -Create a new database and a new user/password (using your favorite -database server) and edit the default configuration file -(:file:`/etc/spamassassin/local.cf`) to add the following lines -inside: - -.. sourcecode:: perl - - bayes_store_module Mail::SpamAssassin::BayesStore:: - bayes_sql_dsn - bayes_sql_username - bayes_sql_password - -Replace values between ``<>`` by yours. Possible values for ``Driver`` -are ``PgSQL`` or ``MySQL`` (non exhaustive list). The syntax for -``DSN`` depends on the driver you choose. Please consult the official -documentation. - -Enable the feature through Modoboa ----------------------------------- - -Manual learning is disabled by default. You can activate it through -the administration panel (*Modoboa > Paremeters > Amavis*). There two -learning levels: - -#. Global: available to administrators only. A single (global) bayes - database is shared between everyone. - -#. Per domain: available to administrators and domain - administrators. Each domain can have a dedicated database. - -#. Per user: each user can create its own database to customize the - way SpamAssassin will detect spam. - -The domain and user levels are not activated by default, dedicated -parameters are available through the panel. - -.. note:: - - Domain and user databases are only created the first time someone - calls the learning feature through the quarantine. - -.. warning:: - - A bayes database needs to reach pre-defined thresholds before it - can be used by SpamAssassin. The default values are **200** spams - and **200** hams. - -You will find other paramaters related to this feature. You won't need -to change them most of the time, unless SpamAssassin is hosted on a -different machine than Modoboa. (in this case, ``spamc`` will be used -instead of ``sa-learn``). +`Official documentation `_. diff --git a/doc/plugins/limits.rst b/doc/plugins/limits.rst index 94b6b240a..e190b6c28 100644 --- a/doc/plugins/limits.rst +++ b/doc/plugins/limits.rst @@ -2,15 +2,4 @@ Per-admin limits ################ -This plugin offers a way to define limits about how many objects -(aliases, mailboxes) a domain administrator can create. - -It also brings a new administrative role: ``Reseller``. A reseller is a domain -administrator that can also manipulate domains and assign permissions -to domain administrators. - -If you don't want to limit a particular object type, just set the -associated value to -1. - -Default limits applied to new administrators can be changed through -the *Modoboa > Parameters > Limits* page. +`Official documentation `_. diff --git a/doc/plugins/postfix_autoreply.rst b/doc/plugins/postfix_autoreply.rst index 0e87414b0..e1683a6b6 100644 --- a/doc/plugins/postfix_autoreply.rst +++ b/doc/plugins/postfix_autoreply.rst @@ -4,48 +4,4 @@ Postfix auto-reply messages ########################### -This plugin let users define an auto-reply message (*vacation*). It is -based on Postfix capabilities. - -The user that executes the autoreply script needs to access -:file:`settings.py`. You must apply proper permissions on this file. For -example, if :file:`settings.py` belongs to ``www-data:www-data``, you can add -the ``vmail`` user to the ``www-data`` group and set the read permission -for the group. - -To make Postfix use this feature, you need to update your -configuration files as follows: - -``/etc/postfix/main.cf``:: - - transport_maps = :/etc/postfix/sql-autoreplies-transport.cf - virtual_alias_maps = :/etc/postfix/sql-aliases.cf - :/etc/postfix/sql-domain-aliases-mailboxes.cf, - :/etc/postfix/sql-autoreplies.cf, - :/etc/postfix/sql-catchall-aliases.cf - -.. note:: - - The order used to define alias maps is important, please respect it - -``/etc/postfix/master.cf``:: - - autoreply unix - n n - - pipe - flags= user=vmail: argv=python /manage.py autoreply $sender $mailbox - -Replace ```` by the name of the database you -use. ```` is the path of your Modoboa instance. - -Then, create the requested map files:: - - $ modoboa-admin.py postfix_maps mapfiles --categories autoreply - -`mapfiles` is the directory where the files will be stored. Answer the -few questions and you're done. - -.. note:: - - Auto-reply messages are just sent one time per sender for a - pre-defined time period. By default, this period is equal to 1 day - (86400s), you can adjust this value by modifying the **Automatic - reply timeout** parameter available in the online panel. +`Official documentation `_. diff --git a/doc/plugins/postfix_relay_domains.rst b/doc/plugins/postfix_relay_domains.rst index 5034f8ef9..7a7c8c7e5 100644 --- a/doc/plugins/postfix_relay_domains.rst +++ b/doc/plugins/postfix_relay_domains.rst @@ -2,39 +2,4 @@ Postfix relay domains support ############################# -This plugin adds the support for relay domains using postfix. You can -use it when the MTA managed by Modoboa is not the final destination of -one or several domains. - -If activated, two new objects will be available from the *Domains* -listing page: *relay domain* and *relay domain alias*. - -The extension is compatible with the *amavis* and *limits* -ones. Resellers will be able to create both new objects. - -Replace by the name of the database you use.To tell Postfix this feature exists, you must generate two new map -files and then update your configuration. - -To generate the map files, run the following command:: - - $ modoboa-admin.py postfix_maps --categories relaydomains --dbtype - -Replace values between ``<>`` by yours. - -Edit the :file:`/etc/postfix/main.cf` file and copy the following -lines inside:: - - relay_domains = :/etc/postfix/sql-relaydomains.cf - transport_maps = - :/etc/postfix/sql-relaydomains-transport.cf - :/etc/postfix/sql-relaydomain-aliases-transport.cf - - smtpd_recipient_restrictions = - permit_mynetworks - reject_unauth_destination - check_recipient_access - :/etc/postfix/sql-relay-recipient-verification.cf - -Replace ```` by the name of the database you use. - -Reload postfix. +`Official documentation `_. diff --git a/doc/plugins/radicale.rst b/doc/plugins/radicale.rst index 511f71aaf..456529d41 100644 --- a/doc/plugins/radicale.rst +++ b/doc/plugins/radicale.rst @@ -2,45 +2,4 @@ Radicale frontend ################# -This plugin provides a simple management frontend for `Radicale -`_, a complete CalDAV (calendar) and CardDAV -(contact) server solution. - -Features currently supported are: - -* Private calendar creation (available for simple users) -* Rights management for private calendars -* Shared calendar creation by domain (available for domain administrators) - -.. note:: - - This plugin requires Radicale 0.9 or higher. - -Setup -===== - -Once `Radicale is installed -`_ on your server, you must -tell Modoboa where it can find the file used to store rules. - -Go to the *Modoboa > Parameters > Radicale* panel and fill the -**Radicale rights file path** setting (an absolute path is required). - -When the configuration is done, Modoboa will completly handles the -file's content. It means every manual modification you could made on -this file would be overriden. - -To do so, a new cron job must be created. You can use the following -example:: - - */2 * * * * /manage.py generate_rights - # - # Or like this if you use a virtual environment: - # */2 * * * * /manage.py generate_rights - -.. note:: - - In some cases, the modifications you make on Modoboa may not be - applied to the rights file. In order to force the generation, - manually run the ``generate_rights`` command using the ``--force`` - option. +`Official documentation `_. diff --git a/doc/plugins/sievefilters.rst b/doc/plugins/sievefilters.rst index 77f77d2f3..0d714afc9 100644 --- a/doc/plugins/sievefilters.rst +++ b/doc/plugins/sievefilters.rst @@ -2,42 +2,4 @@ Sieve filters ############# -This plugin let users create server-side message filters, using the -`sievelib module `_ (which -provides Sieve and ManageSieve clients). - -Two working modes are available: - -* A raw mode: you create filters using the Sieve language directly - (advanced users) -* An assisted mode: you create filters using an intuitive form - -To use this plugin, your hosting setup must include a *ManageSieve* -server and your local delivery agent must understand the *Sieve* -language. Don't panic, Dovecot supports both :-) (refer to -:ref:`dovecot` to know how to enable those features). - -.. note:: - The sieve filters plugin requires that the :ref:`webmail` plugin is - activated and configured. - -Go the online panel and modify the following parameters in order to -communicate with the *ManageSieve* server: - -+--------------------+--------------------+--------------------+ -|Name |Description |Default value | -+====================+====================+====================+ -|Server address |Address of your |127.0.0.1 | -| |MANAGESIEVE server | | -+--------------------+--------------------+--------------------+ -|Server port |Listening port of |4190 | -| |your MANAGESIEVE | | -| |server | | -+--------------------+--------------------+--------------------+ -|Connect using |Use the STARTTLS |no | -|STARTTLS |extension | | -+--------------------+--------------------+--------------------+ -|Authentication |Prefered |auto | -|mechanism |authentication | | -| |mechanism | | -+--------------------+--------------------+--------------------+ +`Official documentation `_. diff --git a/doc/plugins/statistics.rst b/doc/plugins/statistics.rst index 57e821747..50fd452b0 100644 --- a/doc/plugins/statistics.rst +++ b/doc/plugins/statistics.rst @@ -4,44 +4,4 @@ Graphical statistics #################### -This plugin collects various statistics about emails traffic on your -server. It parses a log file to collect information, store it into RRD -files (see `rrdtool `_) and then -generates graphics in PNG format. - -To use it, go to the online parameters panel and adapt the following -ones to your environment: - -+--------------------+--------------------+--------------------------+ -|Name |Description |Default value | -+====================+====================+==========================+ -|Path to the log file|Path to log file |/var/log/mail.log | -| |used to collect | | -| |statistics | | -+--------------------+--------------------+--------------------------+ -|Directory to store |Path to directory |/tmp/modoboa | -|RRD files |where RRD files are | | -| |stored | | -+--------------------+--------------------+--------------------------+ -|Directory to store |Path to directory |/media/stats| -|PNG files |where PNG files are | | -| |stored | | -+--------------------+--------------------+--------------------------+ - -Make sure the directory that will contain RRD files exists. If not, -create it before going further. For example (according to the previous -parameters):: - - $ mkdir /tmp/modoboa - -To finish, you need to collect information periodically in order to -feed the RRD files. Add the following line into root's crontab:: - - */5 * * * * /manage.py logparser &> /dev/null - # - # Or like this if you use a virtual environment: - # 0/5 * * * * /manage.py logparser &> /dev/null - -Replace ```` with the path of your Modoboa instance. - -Graphics will be automatically created after each parsing. +`Official documentation `_. diff --git a/doc/plugins/webmail.rst b/doc/plugins/webmail.rst index 6e5bc8de1..b9df5e984 100644 --- a/doc/plugins/webmail.rst +++ b/doc/plugins/webmail.rst @@ -4,68 +4,4 @@ Webmail ####### -Modoboa provides a simple webmail: - -* Browse, read and compose messages, attachments are supported -* HTML messages are supported -* `CKeditor `_ integration -* Manipulate mailboxes (create, move, remove) -* Quota display - -To use it, go to the online panel and modify the following parameters -to communicate with your *IMAP* server (under *IMAP settings*): - -+--------------------+--------------------+--------------------+ -|Name |Description |Default value | -+====================+====================+====================+ -|Server address |Address of your IMAP|127.0.0.1 | -| |server | | -+--------------------+--------------------+--------------------+ -|Use a secured |Use a secured |no | -|connection |connection to access| | -| |IMAP server | | -+--------------------+--------------------+--------------------+ -|Server port |Listening port of |143 | -| |your IMAP server | | -+--------------------+--------------------+--------------------+ - -Do the same to communicate with your SMTP server (under *SMTP settings*): - -+--------------------+--------------------+--------------------+ -|Name |Description |Default value | -+====================+====================+====================+ -|Server address |Address of your SMTP|127.0.0.1 | -| |server | | -+--------------------+--------------------+--------------------+ -|Secured connection |Use a secured |None | -|mode |connection to access| | -| |SMTP server | | -+--------------------+--------------------+--------------------+ -|Server port |Listening port of |25 | -| |your SMTP server | | -+--------------------+--------------------+--------------------+ -|Authentication |Server needs |no | -|required |authentication | | -+--------------------+--------------------+--------------------+ - -.. note:: - - The size of each attachment sent with messages is limited. You can - change the default value by modifying the **Maximum attachment - size** parameter. - -Using CKeditor -============== - -Modoboa supports CKeditor to compose HTML messages. To use it, first -download it from `the official website `_, then -extract the tarball:: - - $ cd - $ tar xzf /path/to/ckeditor/tarball.tag.gz -C sitestatic/js/ - -And you're done! - -Now, each user has the possibility to choose between CKeditor and the -raw text editor to compose their messages. (see *User > Settings > -Preferences > Webmail*) +`Official documentation `_. diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index cc91ec6c1..9da184959 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -45,11 +45,6 @@ BASE_EVENTS = [ ] PERMISSIONS = { - "DomainAdmins": [ - ["core", "user", "add_user"], - ["core", "user", "change_user"], - ["core", "user", "delete_user"], - ], "SimpleUsers": [] } @@ -72,15 +67,16 @@ def check_for_new_version(request, include_all): """ Check if a new version of Modoboa is available. """ - from modoboa.core.utils import new_version_available + from modoboa.core.utils import check_for_updates if not request.user.is_superuser: return [] - if new_version_available(request) is None: + status, extensions = check_for_updates(request) + if not status: return [{"id": "newversionavailable"}] if include_all else [] return [{ "id": "newversionavailable", "url": reverse("core:index") + "#info/", - "text": _("New Modoboa version available"), + "text": _("One or more updates are available"), "level": "info", }] diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index e3802e229..071e6102e 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -5,9 +5,14 @@ import getpass import os import shutil +import subprocess import sys -import subprocess +try: + import pip +except ImportError: + sys.stderr.write("Error: pip is required to install extensions.\n") + sys.exit(2) import django from django.core import management @@ -16,6 +21,7 @@ from django.template import Context, Template import dj_database_url from modoboa.core.commands import Command +from modoboa.lib.api_client import ModoAPIClient DBCONN_TPL = """ '{{ conn_name }}': { @@ -45,20 +51,13 @@ class DeployCommand(Command): super(DeployCommand, self).__init__(*args, **kwargs) self._parser.add_argument('name', type=str, help='The name of your Modoboa instance') - self._parser.add_argument( - '--with-amavis', action='store_true', default=False, - help='Include amavis configuration' - ) self._parser.add_argument( '--collectstatic', action='store_true', default=False, help='Run django collectstatic command' ) self._parser.add_argument( - '--dburl', type=str, nargs=1, default=None, - help='The database-url for your modoboa instance') - self._parser.add_argument( - '--amavis_dburl', type=str, nargs=1, default=None, - help='The database-url for your amavis instance') + '--dburl', type=str, nargs="+", default=None, + help='A database-url with a name') self._parser.add_argument( '--domain', type=str, default=None, help='The domain under which you want to deploy modoboa') @@ -74,6 +73,14 @@ class DeployCommand(Command): '--devel', action='store_true', default=False, help='Create a development instance' ) + self._parser.add_argument( + '--extensions', type=str, nargs='*', + help="The list of extension to deploy" + ) + self._parser.add_argument( + '--dont-install-extensions', action='store_true', default=False, + help='Do not install extensions using pip' + ) def _exec_django_command(self, name, cwd, *args): """Run a django command for the freshly created project @@ -136,6 +143,29 @@ class DeployCommand(Command): info['PASSWORD'] = getpass.getpass('Password: ') return info + def install_extensions(self, extensions): + """Install one or more extensions. + + Return the list of extensions providing settings we must + include in the final configuration. + + """ + if "all" in extensions: + official_exts = ModoAPIClient().list_extensions() + extensions = [extension["name"] for extension in official_exts] + + pip_args = ["install"] + extensions + pip.main(pip_args) + extra_settings = [] + for extension in extensions: + extension = extension.replace("-", "_") + module = __import__(extension, locals(), globals(), []) + basedir = os.path.dirname(module.__file__) + if not os.path.exists("{0}/settings.py".format(basedir)): + continue + extra_settings.append(extension) + return extra_settings + def handle(self, parsed_args): django.setup() management.call_command( @@ -144,40 +174,24 @@ class DeployCommand(Command): path = "%(name)s/%(name)s" % {'name': parsed_args.name} sys.path.append(parsed_args.name) - t = Template(DBCONN_TPL) - + conn_tpl = Template(DBCONN_TPL) + connections = {} if parsed_args.dburl: - info = dj_database_url.config(default=parsed_args.dburl[0]) - # In case the user fails to supply a valid database url, - # fallback to manual mode - if not info: - print "There was a problem with your database-url. \n" - info = self.ask_db_info() - # If we set this earlier, our fallback method will never - # be triggered - info['conn_name'] = 'default' + for dburl in parsed_args.dburl: + conn_name, url = dburl.split(":", 1) + info = dj_database_url.config(default=url) + # In case the user fails to supply a valid database url, + # fallback to manual mode + if not info: + print "There was a problem with your database-url. \n" + info = self.ask_db_info(conn_name) + # If we set this earlier, our fallback method will never + # be triggered + info['conn_name'] = conn_name + connections[conn_name] = conn_tpl.render(Context(info)) else: - info = self.ask_db_info() - - default_conn = t.render(Context(info)) - - if parsed_args.with_amavis: - if parsed_args.amavis_dburl: - amavis_info = dj_database_url.config( - default=parsed_args.amavis_dburl[0]) - # In case the user fails to supply a valid database - # url, fallback to manual mode - if not amavis_info: - amavis_info = self.ask_db_info('amavis') - # If we set this earlier, our fallback method will - # never be triggered - amavis_info['conn_name'] = 'amavis' - else: - amavis_info = self.ask_db_info('amavis') - - amavis_conn = t.render(Context(amavis_info)) - else: - amavis_conn = None + connections["default"] = conn_tpl.render( + Context(self.ask_db_info())) if parsed_args.domain: allowed_host = parsed_args.domain @@ -188,6 +202,10 @@ class DeployCommand(Command): if not allowed_host: allowed_host = "localhost" + extra_settings = [] + if parsed_args.dont_install_extensions and parsed_args.extensions: + extra_settings = self.install_extensions(parsed_args.extensions) + bower_components_dir = os.path.realpath( os.path.join(os.path.dirname(__file__), "../../bower_components") ) @@ -195,14 +213,16 @@ class DeployCommand(Command): mod = __import__(parsed_args.name, globals(), locals(), ['settings']) tpl = self._render_template( "%s/settings.py.tpl" % self._templates_dir, { - 'default_conn': default_conn, 'amavis_conn': amavis_conn, + 'db_connections': connections, 'secret_key': mod.settings.SECRET_KEY, 'name': parsed_args.name, 'allowed_host': allowed_host, 'lang': parsed_args.lang, 'timezone': parsed_args.timezone, 'bower_components_dir': bower_components_dir, - 'devmode': parsed_args.devel + 'devmode': parsed_args.devel, + 'extensions': parsed_args.extensions, + 'extra_settings': extra_settings } ) with open("%s/settings.py" % path, "w") as fp: diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index 81a5f3fa5..ddcbed991 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -4,25 +4,46 @@ import datetime import getpass -import inspect import os import sys from django.template import Context, Template +import dj_database_url + +from modoboa.lib.api_client import ModoAPIClient from . import Command - -class MapFilesGenerator(object): - - def __init__(self, dbinfo=None): - """Constructor.""" - self.template = Template("""user = {{ dbuser }} +MAP_FILE_TEMPLATE = """user = {{ dbuser }} password = {{ dbpass }} dbname = {{ dbname }} hosts = {{ dbhost }} query = {{ query|safe }} -""") +""" + + +class MapFilesRegistry(object): + + """A registry of all map files.""" + + def __init__(self): + self.files = [] + + def add_files(self, files): + """Add map files to registry.""" + self.files += files + + +registry = MapFilesRegistry() + + +class MapFilesGenerator(object): + + """Map files generator.""" + + def __init__(self, dbinfo=None): + """Constructor.""" + self.template = Template(MAP_FILE_TEMPLATE) if dbinfo is None: self.tpl_context = { 'dbhost': raw_input('Database host: '), @@ -61,21 +82,19 @@ query = {{ query|safe }} ' '.join(sys.argv[1:])) print >> fp, content - def __render_std_maps(self, args): - """Render standard map files. + def __load_extensions(self, extensions): + """Load specified extensions.""" + if "all" in extensions: + # Retrieve extension list from the API + official_exts = ModoAPIClient().list_extensions() + extensions = [extension["name"] for extension in official_exts] - (ie. the ones belonging to the 'std' category) - - :param args: command line arguments - """ - for v in globals().values(): - if not inspect.isclass(v) or not issubclass(v, MapFile): - continue - if v is MapFile: - continue - if v.category not in args.categories: - continue - self.__render_map(args, v) + for extension in extensions: + extension = extension.replace("-", "_") + try: + __import__(extension, locals(), globals(), ["postfix_maps"]) + except ImportError: + sys.stderr.write("Unknown extension {0}".format(extension)) def render(self, args): """Render all map files. @@ -86,11 +105,16 @@ query = {{ query|safe }} os.mkdir(args.destdir) except OSError: pass - self.__render_std_maps(args) + if args.extensions: + self.__load_extensions(args.extensions) + for mapfile in registry.files: + self.__render_map(args, mapfile) class SQLiteMapFilesGenerator(MapFilesGenerator): + """SQLite special generator.""" + def __init__(self, dbinfo=None): self.template = Template("""dbpath = {{ dbpath }} query = {{ query|safe }} @@ -105,112 +129,10 @@ query = {{ query|safe }} } -class MapFile(object): - category = 'std' - - -class DomainsMap(MapFile): - filename = 'sql-domains.cf' - mysql = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" - postgres = "SELECT name FROM admin_domain WHERE name='%s' AND enabled" - sqlite = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" - - -class DomainsAliasesMap(MapFile): - filename = 'sql-domain-aliases.cf' - mysql = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" - postgres = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled AND dom.enabled" - sqlite = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" - - -class AliasesMap(MapFile): - filename = 'sql-aliases.cf' - mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT concat(al.address, '@', dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" - postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.address || '@' || dom.name FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled AND al.extmboxes<>'')" - sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT (al.address || '@' || dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" - - -class DomainAliasesMailboxesMap(MapFile): - filename = 'sql-domain-aliases-mailboxes.cf' - mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT concat(al.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" - postgres = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND al.address='%u')" - sqlite = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" - - -class MailboxesSelfAliasesMap(MapFile): - filename = "sql-mailboxes-self-aliases.cf" - mysql = "SELECT email FROM core_user u INNER JOIN admin_mailbox mb ON mb.user_id=u.id WHERE u.email='%s' AND u.is_active=1" - postgres = "SELECT email FROM core_user u INNER JOIN admin_mailbox mb ON mb.user_id=u.id WHERE u.email='%s' AND u.is_active" - sqlite = "SELECT email FROM core_user u INNER JOIN admin_mailbox mb ON mb.user_id=u.id WHERE u.email='%s' AND u.is_active=1" - - -class CatchallAliasesMap(MapFile): - filename = 'sql-catchall-aliases.cf' - mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" - postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled AND al.address='*' AND dom.name='%d' AND dom.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled)" - sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" - - -class MaintainMap(MapFile): - filename = 'sql-maintain.cf' - mysql = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" - postgres = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" - sqlite = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" - - -class TransportMap(MapFile): - category = "autoreply" - filename = 'sql-autoreplies-transport.cf' - mysql = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" - postgres = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" - sqlite = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" - - -class AutoRepliesMap(MapFile): - category = "autoreply" - filename = 'sql-autoreplies.cf' - mysql = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" - postgres = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" - sqlite = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" - - -class RelayDomainsMap(MapFile): - category = "relaydomains" - filename = "sql-relaydomains.cf" - mysql = "SELECT name FROM postfix_relay_domains_relaydomain WHERE name='%s' AND enabled=1" - postgres = "SELECT name FROM postfix_relay_domains_relaydomain WHERE name='%s' AND enabled" - sqlite = "SELECT name FROM postfix_relay_domains_relaydomain WHERE name='%s' AND enabled=1" - - -class RelayDomainsTransportMap(MapFile): - category = "relaydomains" - filename = "sql-relaydomains-transport.cf" - mysql = "SELECT CONCAT(srv.name, ':[', rdom.target_host, ']') FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id WHERE rdom.enabled=1 AND rdom.name='%s'" - postgres = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id WHERE rdom.enabled AND rdom.name='%s'" - sqlite = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id WHERE rdom.enabled=1 AND rdom.name='%s'" - - -class RelayDomainAliasesTransportMap(MapFile): - category = "relaydomains" - filename = "sql-relaydomain-aliases-transport.cf" - mysql = "SELECT CONCAT(srv.name, ':[', rdom.target_host, ']') FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id INNER JOIN postfix_relay_domains_relaydomainalias AS rdomalias ON rdom.id=rdomalias.target_id WHERE rdom.enabled=1 AND rdomalias.enabled=1 AND rdomalias.name='%s'" - postgres = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id INNER JOIN postfix_relay_domains_relaydomainalias AS rdomalias ON rdom.id=rdomalias.target_id WHERE rdom.enabled AND rdomalias.enabled AND rdomalias.name='%s'" - sqlite = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id INNER JOIN postfix_relay_domains_relaydomainalias AS rdomalias ON rdom.id=rdomalias.target_id WHERE rdom.enabled=1 AND rdomalias.enabled=1 AND rdomalias.name='%s'" - - -class RelayRecipientVerification(MapFile): - category = "relaydomains" - filename = "sql-relay-recipient-verification.cf" - mysql = "SELECT 'reject_unverified_recipient' FROM postfix_relay_domains_relaydomain WHERE verify_recipients=1 AND name='%d'" - postgres = "SELECT 'reject_unverified_recipient' FROM postfix_relay_domains_relaydomain WHERE verify_recipients AND name='%d'" - sqlite = ( - "SELECT 'reject_unverified_recipient' " - "FROM postfix_relay_domains_relaydomain " - "WHERE verify_recipients=1 AND name='%d'" - ) - - class PostfixMapsCommand(Command): + + """Base command to generate map files.""" + help = "Generate ready-to-use postfix map files" def __init__(self, *args, **kwargs): @@ -229,16 +151,17 @@ class PostfixMapsCommand(Command): help="url of Modoboa's database" ) self._parser.add_argument( - '--categories', type=str, nargs='*', default=['std'], - help='Map file categories to generate (choices: std, autoreply)' + '--extensions', type=str, nargs='+', + help=( + 'Generate map files for this extensions ' + '("all" shortcut available)' + ) ) def handle(self, parsed_args): - """Command entry point. - """ + """Command entry point.""" dbinfo = None if parsed_args.dburl: - import dj_database_url dbinfo = dj_database_url.config(default=parsed_args.dburl[0]) if 'sqlite' in dbinfo['ENGINE']: parsed_args.dbtype = 'sqlite' @@ -247,7 +170,7 @@ class PostfixMapsCommand(Command): else: parsed_args.dbtype = 'mysql' if parsed_args.dbtype == 'sqlite': - g = SQLiteMapFilesGenerator(dbinfo) + generator = SQLiteMapFilesGenerator(dbinfo) else: - g = MapFilesGenerator(dbinfo) - g.render(parsed_args) + generator = MapFilesGenerator(dbinfo) + generator.render(parsed_args) diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 2ba6dc86b..8ba295fc8 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -57,18 +57,8 @@ MODOBOA_APPS = ( 'modoboa.core', 'modoboa.lib', # Modoboa extensions here. - 'modoboa.extensions.admin', - 'modoboa.extensions.postfix_relay_domains', - 'modoboa.extensions.limits', - 'modoboa.extensions.postfix_autoreply', - 'modoboa.extensions.webmail', - 'modoboa.extensions.stats', - 'modoboa.extensions.sievefilters', - 'modoboa.extensions.radicale', - {% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis', - - # Tools from here - #'modoboa.tools.pfxadmin_migrate', +{% for extension in extensions %} '{{ extension }}', +{% endfor %} ) INSTALLED_APPS += MODOBOA_APPS @@ -104,25 +94,10 @@ WSGI_APPLICATION = '{{ name }}.wsgi.application' # Database # https://docs.djangoproject.com/en/1.6/ref/settings/#databases -DATABASES = { {{ default_conn|safe }} - # "pfxadmin" : { - # "ENGINE" : "django.db.backends.", - # "NAME" : "", - # "USER" : "", - # "PASSWORD" : "" - # },{% if not amavis_conn %} - # "amavis": { - # "ENGINE" : "django.db.backends.", - # "HOST" : "", - # "NAME" : "", - # "USER" : "", - # "PASSWORD" : "" - # }{% else %} - {{ amavis_conn|safe }}{% endif %} +DATABASES = { + {% for conn in db_connections.values %}{{ conn|safe }}{% endfor %} } -DATABASE_ROUTERS = ["modoboa.extensions.amavis.dbrouter.AmavisRouter"] - # Internationalization # https://docs.djangoproject.com/en/1.6/topics/i18n/ @@ -188,3 +163,6 @@ LOGGING = { } } } +{% for extension in extra_settings %} +import {{ extension }}.settings +{% endfor %} diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index 82e5fd30e..6b84ce3dc 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -3,6 +3,11 @@ from django.conf import settings from django.conf.urls import include +from versionfield.constants import DEFAULT_NUMBER_BITS +from versionfield.version import Version + +from modoboa.lib.api_client import ModoAPIClient + class ModoExtension(object): @@ -70,6 +75,28 @@ class ExtensionsPool(object): return None return instance.infos() + def load_extension(self, name): + """Load a registered extension.""" + __import__(name, locals(), globals(), ["modo_extension"]) + extinstance = self.get_extension(name) + if extinstance is None: + return None + result = None + try: + baseurl = ( + extinstance.url if extinstance.url is not None + else name + ) + result = ( + r'^%s/' % (baseurl), + include("{0}.urls".format(name), namespace=name) + ) + except ImportError: + # No urls for this extension + pass + extinstance.load() + return result + def load_all(self): """Load all defined extensions. @@ -83,32 +110,34 @@ class ExtensionsPool(object): """ result = [] for ext in settings.MODOBOA_APPS: - __import__(ext, locals(), globals(), ["modo_extension"]) - extname = ext.split('.')[-1] - extinstance = self.get_extension(extname) - if extinstance is None: - continue - try: - baseurl = extinstance.url \ - if extinstance.url is not None else extname - result += [ - (r'^%s/' % (baseurl), - include("{0}.urls".format(ext), namespace=extname)) - ] - except ImportError: - # No urls for this extension - pass - extinstance.load() + ext_urls = self.load_extension(ext) + if ext_urls is not None: + result += [ext_urls] return result - def list_all(self): + def list_all(self, check_new_versions=False): """List all defined extensions.""" result = [] + if check_new_versions: + new_extensions = ModoAPIClient().list_extensions() + new_extensions = dict( + (ext["name"], ext["version"]) for ext in new_extensions + ) + else: + new_extensions = {} for extname, extdef in self.extensions.iteritems(): if not extdef["show"]: continue infos = self.get_extension_infos(extname) infos["id"] = extname + local_version = Version(infos["version"], DEFAULT_NUMBER_BITS) + pkgname = infos["name"].replace("_", "-") + if pkgname in new_extensions: + infos["last_version"] = new_extensions[pkgname] + last_version = Version( + new_extensions[pkgname], DEFAULT_NUMBER_BITS) + if last_version > local_version: + infos["update"] = True result += [infos] return sorted(result, key=lambda i: i["name"]) diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index 341d3f376..e8cdd8229 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -38,7 +38,7 @@ class Command(BaseCommand, CloseConnectionMixin): superadmin = User.objects.filter(is_superuser=True).first() groups = PERMISSIONS.keys() + [ role[0] for role - in events.raiseQueryEvent("GetExtraRoles", superadmin) + in events.raiseQueryEvent("GetExtraRoles", superadmin, None) ] for groupname in groups: group, created = Group.objects.get_or_create(name=groupname) diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 80b77bb15..811d2246e 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -335,11 +335,14 @@ class User(PermissionsMixin): :param row: a list containing the expected information :param crypt_password: """ + from modoboa.lib.permissions import get_account_roles + if len(row) < 7: raise BadRequest(_("Invalid line")) + allowed_roles = get_account_roles(user) + allowed_roles = [role[0] for role in allowed_roles] role = row[6].strip() - allowed_roles = ["SimpleUsers", "DomainAdmins"] - if not user.is_superuser and role not in allowed_roles: + if role not in allowed_roles: raise PermDeniedException( _("You can't import an account with a role greater than yours") ) diff --git a/modoboa/core/templates/core/extensions.html b/modoboa/core/templates/core/extensions.html deleted file mode 100644 index 6415135e3..000000000 --- a/modoboa/core/templates/core/extensions.html +++ /dev/null @@ -1,22 +0,0 @@ -{% load i18n %} -{% load url from future %} - -

{% trans "Extensions" %} {% trans "The extension(s) you are using" %}


- - - - - - - - - - {% for extension in extensions %} - - - - - - {% endfor %} - -
{% trans "Name" %}{% trans "Version" %}{% trans "Description" %}
{{ extension.label }}{{ extension.version }}{{ extension.description }}
diff --git a/modoboa/core/templates/core/information.html b/modoboa/core/templates/core/information.html index 23f6264c2..9d1fff110 100644 --- a/modoboa/core/templates/core/information.html +++ b/modoboa/core/templates/core/information.html @@ -1,25 +1,50 @@ {% load i18n core_tags lib_tags %}

{% trans "Information" %} {% trans "Various information about Modoboa" %}


-{% if new_version %} +{% if update_avail %}
-

{% trans "An update is available!" %}

- {% blocktrans with version=new_version.version changelog_url=new_version.changelog_url %} - Modoboa {{ version }} is available for download. The list of all changes is available here. - {% endblocktrans %} +

{% trans "One or more updates are available!" %}

+

{% trans "Check the following list to find the component(s) that can be updated." %}

{% endif %} -
Modoboa {% get_version %} {% trans "is currently in use." %}
+ +

{% trans "Applications" %}

+ + + + + + + + + + + {% for extension in extensions %} + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Installed version" %}{% trans "Last version" %}{% trans "Description" %}
{{ extension.label }}{{ extension.version }}{% if extension.update %}{% if extension.changelog_url %}{{ extension.last_version }}{% else %}{{ extension.last_version }}{% endif %}{% else %}{{ extension.last_version }}{% endif %}{{ extension.description }}

{% trans "Active users" %}

- - - - - - - {% connected_users as users %}{% for u in users %} - - - - {% endfor %} +
{% trans "Name" %}{% trans "Group name" %}{% trans "Last login" %}
{{ u.username }}{{ u.group }}{{ u.last_login }}
+ + + + + + + + + {% connected_users as users %}{% for u in users %} + + + + + + {% endfor %} +
{% trans "Name" %}{% trans "Group name" %}{% trans "Last login" %}
{{ u.username }}{{ u.group }}{{ u.last_login }}
diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index bd50c7eb7..805fa289c 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -46,11 +46,6 @@ def extensions_menu(selection, user): @register.simple_tag def admin_menu(selection, user): entries = [ - {"name": "extensions", - "class": "ajaxnav", - "url": "extensions/", - "label": _("Extensions"), - "img": ""}, {"name": "info", "class": "ajaxnav", "url": "info/", @@ -151,7 +146,7 @@ def visirule(field): % (rule["field"], rule["value"]) -@register.simple_tag +@register.assignment_tag def get_version(): import pkg_resources return pkg_resources.get_distribution("modoboa").version diff --git a/modoboa/core/tests.py b/modoboa/core/tests.py index 8c3608dae..ee070a693 100644 --- a/modoboa/core/tests.py +++ b/modoboa/core/tests.py @@ -1,16 +1,12 @@ -import os -import tempfile +"""Tests for core application.""" from django.core.urlresolvers import reverse -from django.test import TestCase -from modoboa.lib.sysutils import exec_cmd from modoboa.lib.tests import ModoTestCase from . import factories class ProfileTestCase(ModoTestCase): - fixtures = ['initial_users.json'] def setUp(self): super(ProfileTestCase, self).setUp() @@ -46,59 +42,3 @@ class ProfileTestCase(ModoTestCase): self.assertTrue( self.clt.login(username="user@test.com", password="tutu") ) - - -class ModoboaAdminCommandTestCase(TestCase): - MAP_FILES = { - "std": [ - "sql-domains.cf", "sql-domain-aliases.cf", "sql-aliases.cf", - "sql-domain-aliases-mailboxes.cf", "sql-catchall-aliases.cf", - "sql-maintain.cf" - ], - "autoreply": ["sql-autoreplies-transport.cf", "sql-autoreplies.cf"], - "relaydomains": [ - "sql-relaydomains.cf", "sql-relaydomains-transport.cf", - "sql-relaydomain-aliases-transport.cf", - "sql-relay-recipient-verification.cf" - ] - } - - def setUp(self): - self.workdir = tempfile.mkdtemp() - - def tearDown(self): - exec_cmd("rm -rf {0}".format(self.workdir)) - - def _test_maps_generation(self, engine, categories=None): - if categories is None: - categories = ["std", "autoreply", "relaydomains"] - dburl = "{0}://user:password@localhost/testdb".format(engine) - code, output = exec_cmd( - "modoboa-admin.py postfix_maps --categories {0}" - " --dburl {1} {2}".format( - " ".join(categories), dburl, self.workdir - ) - ) - self.assertEqual(code, 0) - for category in categories: - for mapfile in self.MAP_FILES[category]: - path = "{0}/{1}".format(self.workdir, mapfile) - self.assertTrue(os.path.exists(path)) - with open(path) as fpo: - content = fpo.read() - if engine != "sqlite": - self.assertIn("user = user", content) - self.assertIn("password = password", content) - self.assertIn("dbname = testdb", content) - self.assertIn("hosts = localhost", content) - else: - self.assertIn("dbpath = testdb", content) - - def test_postgres_maps(self): - self._test_maps_generation("postgres") - - def test_mysql_maps(self): - self._test_maps_generation("mysql") - - def test_sqlite_maps(self): - self._test_maps_generation("sqlite") diff --git a/modoboa/core/urls.py b/modoboa/core/urls.py index cf03287fa..e4fba293d 100644 --- a/modoboa/core/urls.py +++ b/modoboa/core/urls.py @@ -15,8 +15,6 @@ urlpatterns = patterns( name="parameter_listl"), url(r'^core/parameters/save/$', 'modoboa.core.views.admin.saveparameters', name="parameter_save"), - url(r'^core/extensions/$', 'modoboa.core.views.admin.viewextensions', - name="extension_list"), url(r'^core/info/$', 'modoboa.core.views.admin.information', name="information"), url(r'^core/logs/$', 'modoboa.core.views.admin.logs', name="log_list"), diff --git a/modoboa/core/utils.py b/modoboa/core/utils.py index 979654b14..fef7450b2 100644 --- a/modoboa/core/utils.py +++ b/modoboa/core/utils.py @@ -1,40 +1,31 @@ -""" -Utility functions. -""" -import os -import pkg_resources +"""Utility functions.""" -import requests -from requests.exceptions import RequestException - -from django.conf import settings +from django.utils.translation import ugettext as _ from django.contrib.sites.models import get_current_site -from versionfield.constants import DEFAULT_NUMBER_BITS -from versionfield.version import Version - +from modoboa.core.extensions import exts_pool from modoboa.lib import parameters +from modoboa.lib.api_client import ModoAPIClient -def new_version_available(request): +def check_for_updates(request): """Check if a new version of Modoboa is available.""" if parameters.get_admin("CHECK_NEW_VERSIONS") == "no": - return None - local_version = pkg_resources.get_distribution("modoboa").version - url = os.path.join(settings.MODOBOA_API_URL, "current_version/") - site = get_current_site(request) - try: - resp = requests.get(url, params={ - "client_version": pkg_resources.get_distribution("modoboa").version, - "client_site": site - }) - except RequestException: - return None - if resp.status_code != 200: - return None - resp = resp.json() - version = Version(resp["version"], DEFAULT_NUMBER_BITS) - if version <= Version(local_version, DEFAULT_NUMBER_BITS): - return None - return resp + return False, [] + client = ModoAPIClient() + extensions = exts_pool.list_all(True) + extensions = [{ + "label": "Modoboa", + "description": _("The core part of Modoboa"), + "update": client.new_core_version(get_current_site(request)), + "last_version": client.latest_core_version, + "changelog_url": client.changelog_url, + "version": client.local_core_version + }] + extensions + update_avail = False + for extension in extensions: + if "update" in extension: + update_avail = True + break + return update_avail, extensions diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index f1844ff76..56d0b19df 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -8,8 +8,9 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.utils.translation import ugettext as _ +from modoboa.core.extensions import exts_pool from modoboa.core.models import Log -from modoboa.core.utils import new_version_available +from modoboa.core.utils import check_for_updates from modoboa.lib import events, parameters from modoboa.lib.listing import get_sort_order, get_listing_page from modoboa.lib.web_utils import ( @@ -51,25 +52,14 @@ def saveparameters(request): return render_to_json_response(_("Parameters saved")) -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewextensions(request, tplname='core/extensions.html'): - """List available extensions.""" - from modoboa.core.extensions import exts_pool - - exts = exts_pool.list_all() - return render_to_json_response({ - "callback": "extensions", - "content": _render_to_string(request, tplname, {"extensions": exts}) - }) - - @login_required @user_passes_test(lambda u: u.is_superuser) def information(request, tplname="core/information.html"): + status, extensions = check_for_updates(request) return render_to_json_response({ "content": render_to_string(tplname, { - "new_version": new_version_available(request) + "update_avail": status, + "extensions": extensions, }), }) diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index 44325a366..e515cb61f 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -41,7 +41,8 @@ def dologin(request): if user.group == "SimpleUsers": nextlocation = reverse("topredirection") else: - nextlocation = reverse("admin:domain_list") + # FIXME + nextlocation = reverse("modoboa_admin:domain_list") return HttpResponseRedirect(nextlocation) error = _( "Your username and password didn't match. Please try again.") diff --git a/modoboa/extensions/__init__.py b/modoboa/extensions/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/admin/app_settings.py b/modoboa/extensions/admin/app_settings.py deleted file mode 100644 index 8638bce9e..000000000 --- a/modoboa/extensions/admin/app_settings.py +++ /dev/null @@ -1,100 +0,0 @@ -from django import forms -from django.conf import settings -from django.utils.translation import ugettext_lazy -from modoboa.lib.form_utils import YesNoField, SeparatorField -from modoboa.lib.sysutils import exec_cmd -from modoboa.lib import parameters -import os - - -class AdminParametersForm(parameters.AdminParametersForm): - app = "admin" - - mbsep = SeparatorField(label=ugettext_lazy("Mailboxes")) - - handle_mailboxes = YesNoField( - label=ugettext_lazy("Handle mailboxes on filesystem"), - initial="no", - help_text=ugettext_lazy( - "Rename or remove mailboxes on the filesystem when they get" - " renamed or removed within Modoboa" - ) - ) - - mailboxes_owner = forms.CharField( - label=ugettext_lazy("Mailboxes owner"), - initial="vmail", - help_text=ugettext_lazy( - "The UNIX account who owns mailboxes on the filesystem" - ) - ) - - default_domain_quota = forms.IntegerField( - label=ugettext_lazy("Default domain quota"), - initial=0, - help_text=ugettext_lazy( - "Default quota (in MB) applied to freshly created domains with no " - "value specified. A value of 0 means no quota." - ) - ) - - auto_account_removal = YesNoField( - label=ugettext_lazy("Automatic account removal"), - initial="no", - help_text=ugettext_lazy( - "When a mailbox is removed, also remove the associated account") - ) - - auto_create_domain_and_mailbox = YesNoField( - label=ugettext_lazy("Automatic domain/mailbox creation"), - initial="yes", - help_text=ugettext_lazy( - "Create a domain and a mailbox when an account is automatically " - "created." - ) - ) - - # Visibility rules - visibility_rules = { - "mailboxes_owner": "handle_mailboxes=yes", - } - - def __init__(self, *args, **kwargs): - super(AdminParametersForm, self).__init__(*args, **kwargs) - self.field_widths = { - "default_domain_quota": 2 - } - hide_fields = False - dpath = None - code, output = exec_cmd("which dovecot") - if not code: - dpath = output.strip() - else: - known_paths = getattr( - settings, "DOVECOT_LOOKUP_PATH", - ("/usr/sbin/dovecot", "/usr/local/sbin/dovecot") - ) - for fpath in known_paths: - if os.path.isfile(fpath) and os.access(fpath, os.X_OK): - dpath = fpath - if dpath: - try: - code, version = exec_cmd("%s --version" % dpath) - except OSError: - hide_fields = True - else: - if code or not version.strip().startswith("2"): - hide_fields = True - else: - hide_fields = True - if hide_fields: - del self.fields["handle_mailboxes"] - del self.fields["mailboxes_owner"] - - def clean_default_domain_quota(self): - """Ensure quota is a positive integer.""" - if self.cleaned_data['default_domain_quota'] < 0: - raise forms.ValidationError( - ugettext_lazy('Must be a positive integer') - ) - return self.cleaned_data['default_domain_quota'] diff --git a/modoboa/extensions/admin/factories.py b/modoboa/extensions/admin/factories.py deleted file mode 100644 index e49956e83..000000000 --- a/modoboa/extensions/admin/factories.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Fixtures factories. -""" -import factory - -from modoboa.core.factories import PermissionFactory, UserFactory -from . import models - - -class DomainFactory(PermissionFactory): - - """Factory to create Domains.""" - - class Meta: - model = models.Domain - django_get_or_create = ("name", ) - - quota = 10 - enabled = True - - -class MailboxFactory(PermissionFactory): - - class Meta: - model = models.Mailbox - - domain = factory.SubFactory(DomainFactory) - user = factory.SubFactory(UserFactory) - quota = 10 - enabled = True - - -class AliasFactory(PermissionFactory): - - class Meta: - model = models.Alias - - enabled = True - - -def populate_database(): - """Create test data. - - 2 domains, 1 domain admin, 2 users - """ - dom = DomainFactory(name="test.com") - admin = UserFactory( - username="admin@test.com", groups=('DomainAdmins', ), - password='{PLAIN}toto' - ) - MailboxFactory(address='admin', domain=dom, user=admin) - account = UserFactory.create( - username="user@test.com", groups=('SimpleUsers',), - ) - MailboxFactory.create(address='user', domain=dom, user=account) - - fwd = AliasFactory.create( - address='forward', domain=dom, extmboxes='user@external.com' - ) - al = AliasFactory.create( - address='alias', domain=dom - ) - al.mboxes.add(account.mailbox_set.all()[0]) - AliasFactory.create( - address='postmaster', domain=dom, extmboxes='toto@titi.com,test@truc.fr' - ) - dom.add_admin(admin) - - dom2 = DomainFactory.create(name='test2.com', quota=0) - admin = UserFactory.create( - username='admin@test2.com', groups=('DomainAdmins',), - password='{PLAIN}toto' - ) - MailboxFactory.create(address='admin', domain=dom2, user=admin) - u = UserFactory.create( - username='user@test2.com', groups=('SimpleUsers',) - ) - MailboxFactory.create(address='user', domain=dom2, user=u) - dom2.add_admin(admin) diff --git a/modoboa/extensions/admin/fixtures/demo.json b/modoboa/extensions/admin/fixtures/demo.json deleted file mode 100644 index d3691219b..000000000 --- a/modoboa/extensions/admin/fixtures/demo.json +++ /dev/null @@ -1,310 +0,0 @@ -[ - { - "pk": 1, - "model": "core.user", - "fields": { - "username": "admin", - "first_name": "", - "last_name": "", - "is_active": true, - "is_superuser": true, - "is_staff": false, - "last_login": "2012-05-03T16:58:51+01:00", - "groups": [], - "user_permissions": [], - "password": "{CRYPT}dTTsGDkA5ZHKg", - "email": "", - "date_joined": "2012-01-26T15:19:25+01:00" - } - }, - { - "pk": 2, - "model": "core.user", - "fields": { - "username": "admin@demo.local", - "first_name": "Demo", - "last_name": "Administrator", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2012-05-24T05:00:12+01:00", - "groups": [1], - "user_permissions": [], - "password": "{CRYPT}SJ2bQWwabfH/6", - "email": "admin@demo.local", - "date_joined": "2012-05-03T16:50:15+01:00" - } - }, - { - "pk": 4, - "model": "core.user", - "fields": { - "username": "user@demo.local", - "first_name": "Demo", "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2012-05-03T16:56:50+01:00", - "groups": [2], - "user_permissions": [], - "password": "{CRYPT}2iAFXtxUA6.7Y", - "email": "user@demo.local", - "date_joined": "2012-05-03T16:56:50+01:00" - } - }, - { - "pk": 2, - "model": "core.objectaccess", - "fields": { - "is_owner": true, - "user": 1, - "content_type": ["admin", "domain"], - "object_id": 1 - } - }, - { - "pk": 3, - "model": "core.objectaccess", - "fields": { - "is_owner": true, - "user": 1, - "content_type": ["core", "user"], - "object_id": 2 - } - }, - { - "pk": 4, - "model": "core.objectaccess", - "fields": { - "is_owner": true, - "user": 1, - "content_type": ["admin", "mailbox"], - "object_id": 1 - } - }, - { - "pk": 5, - "model": "core.objectaccess", - "fields": { - "is_owner": true, - "user": 1, - "content_type": ["admin", "alias"], - "object_id": 1 - } - }, - { - "pk": 6, - "model": "core.objectaccess", - "fields": { - "is_owner": false, - "user": 2, - "content_type": ["admin", "domain"], - "object_id": 1 - } - }, - { - "pk": 7, - "model": "core.objectaccess", - "fields": { - "is_owner": false, - "user": 2, - "content_type": ["admin", "mailbox"], - "object_id": 1 - } - }, - { - "pk": 8, - "model": "core.objectaccess", - "fields": { - "is_owner": false, - "user": 2, - "content_type": ["core", "user"], - "object_id": 2 - } - }, - { - "pk": 9, - "model": "core.objectaccess", - "fields": { - "is_owner": false, - "user": 2, - "content_type": ["admin", - "alias"], - "object_id": 1}}, - { - "pk": 13, - "model": "core.objectaccess", - "fields": { - "is_owner": true, - "user": 1, - "content_type": ["core", - "user"], - "object_id": 4}}, - { - "pk": 14, - "model": "core.objectaccess", - "fields": { - "is_owner": true, - "user": 1, - "content_type": ["admin", - "mailbox"], - "object_id": 3}}, - { - "pk": 15, - "model": "core.objectaccess", - "fields": { - "is_owner": false, - "user": 2, - "content_type": ["admin", - "mailbox"], - "object_id": 3}}, - { - "pk": 16, - "model": "core.objectaccess", - "fields": { - "is_owner": false, - "user": 2, - "content_type": ["core", - "user"], - "object_id": 4}}, - { - "pk": 1, - "model": "admin.objectdates", - "fields": { - "creation": "2012-05-03T16:49:40+01:00", - "last_modification": "2012-05-03T16:49:40+01:00"}}, - { - "pk": 2, - "model": "admin.objectdates", - "fields": { - "creation": "2012-05-03T16:50:15+01:00", - "last_modification": "2012-05-03T16:50:36+01:00"}}, - { - "pk": 3, - "model": "admin.objectdates", - "fields": { - "creation": "2012-05-03T16:50:15+01:00", - "last_modification": "2012-05-03T16:50:15+01:00"}}, - { - "pk": 4, - "model": "admin.objectdates", - "fields": { - "creation": "2012-05-03T16:51:04+01:00", - "last_modification": "2012-05-03T16:51:04+01:00"}}, - { - "pk": 5, - "model": "admin.objectdates", - "fields": { - "creation": "2012-05-03T16:56:50+01:00", - "last_modification": "2012-05-03T16:56:50+01:00"}}, - { - "pk": 1, - "model": "admin.domain", - "fields": { - "dates": 1, - "enabled": true, - "name": "demo.local", - "quota": 10}}, - { - "pk": 1, - "model": "admin.mailbox", - "fields": { - "dates": 2, - "quota": 10, - "user": 2, - "domain": 1, - "address": "admin" - } - }, - { - "pk": 3, - "model": "admin.mailbox", - "fields": { - "dates": 5, - "quota": 10, - "user": 4, - "domain": 1, - "address": "user" - } - }, - { - "pk": 1, - "model": "admin.alias", - "fields": { - "dates": 3, - "enabled": true, - "domain": 1, - "mboxes": [1], - "address": "postmaster", - "extmboxes": ""}}, - { - "pk": 1, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "limits"}}, - { - "pk": 2, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "postfix_autoreply"}}, - { - "pk": 3, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "stats"}}, - { - "pk": 4, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "amavis"}}, - { - "pk": 5, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "webmail"}}, - { - "pk": 6, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "sievefilters"}}, - { - "pk": 7, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "postfix_relay_domains" - } - }, - { - "pk": 8, - "model": "core.extension", - "fields": { - "enabled": true, - "name": "radicale" - } - }, - { - "pk": "admin@demo.local", - "model": "admin.quota", - "fields": { - "username": "admin@demo.local", - "bytes": 0, - "messages": 0 - } - }, - { - "pk": "user@demo.local", - "model": "admin.quota", - "fields": { - "username": "user@demo.local", - "bytes": 0, - "messages": 0 - } - } -] diff --git a/modoboa/extensions/admin/forms/__init__.py b/modoboa/extensions/admin/forms/__init__.py deleted file mode 100644 index dbb5a36ef..000000000 --- a/modoboa/extensions/admin/forms/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from .domain import ( - DomainFormGeneral, DomainFormOptions, DomainForm, DomainWizard -) -from .account import ( - AccountFormGeneral, AccountFormMail, AccountPermissionsForm, - AccountForm, AccountWizard -) -from .alias import AliasForm -from .forward import ForwardForm -from .import_ import ImportDataForm, ImportIdentitiesForm -from .export import ExportDataForm, ExportDomainsForm, ExportIdentitiesForm - -__all__ = [ - 'DomainFormGeneral', 'DomainFormOptions', 'DomainForm', - 'AccountFormGeneral', 'AccountFormMail', 'AccountPermissionsForm', - 'AccountForm', 'AliasForm', 'ImportDataForm', 'ImportIdentitiesForm', - 'ExportDataForm', 'ExportDomainsForm', 'ExportIdentitiesForm', - 'ForwardForm', 'DomainWizard', 'AccountWizard' -] diff --git a/modoboa/extensions/admin/forms/account.py b/modoboa/extensions/admin/forms/account.py deleted file mode 100644 index 3d929f697..000000000 --- a/modoboa/extensions/admin/forms/account.py +++ /dev/null @@ -1,478 +0,0 @@ -"""Forms related to accounts management.""" - -from collections import OrderedDict - -from django import forms -from django.core.urlresolvers import reverse -from django.http import QueryDict -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.core.models import User -from modoboa.extensions.admin.models import ( - Domain, Mailbox, Alias -) -from modoboa.lib import events, parameters -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound -from modoboa.lib.form_utils import ( - DomainNameField, DynamicForm, TabForms, WizardForm -) -from modoboa.lib.permissions import get_account_roles -from modoboa.lib.web_utils import render_to_json_response - - -class AccountFormGeneral(forms.ModelForm): - username = forms.CharField( - label=ugettext_lazy("Username"), - help_text=ugettext_lazy( - "The user's name. Must be a valid e-mail address for simple users " - "or administrators with a mailbox." - ) - ) - role = forms.ChoiceField( - label=ugettext_lazy("Role"), - choices=[('', ugettext_lazy("Choose"))], - help_text=ugettext_lazy("What level of permission this user will have") - ) - password1 = forms.CharField( - label=ugettext_lazy("Password"), widget=forms.widgets.PasswordInput - ) - password2 = forms.CharField( - label=ugettext_lazy("Confirmation"), - widget=forms.widgets.PasswordInput, - help_text=ugettext_lazy( - "Enter the same password as above, for verification." - ) - ) - - class Meta: - model = User - fields = ("username", "first_name", "last_name", "role", "is_active") - - def __init__(self, user, *args, **kwargs): - super(AccountFormGeneral, self).__init__(*args, **kwargs) - self.fields = OrderedDict( - (key, self.fields[key]) for key in - ['role', 'username', 'first_name', 'last_name', 'password1', - 'password2', 'is_active'] - ) - self.fields["is_active"].label = _("Enabled") - self.user = user - if user.group == "DomainAdmins": - self.fields["role"] = forms.CharField( - label="", - widget=forms.HiddenInput(attrs={"class": "form-control"}), - required=False - ) - else: - self.fields["role"].choices = [('', ugettext_lazy("Choose"))] - self.fields["role"].choices += \ - get_account_roles(user, kwargs['instance']) \ - if 'instance' in kwargs else get_account_roles(user) - - if "instance" in kwargs: - account = kwargs["instance"] - domain_disabled = account.mailbox_set.count() and \ - not account.mailbox_set.all()[0].domain.enabled - if domain_disabled: - self.fields["is_active"].widget.attrs['disabled'] = "disabled" - if args: - if args[0].get("password1", "") == "" \ - and args[0].get("password2", "") == "": - self.fields["password1"].required = False - self.fields["password2"].required = False - if domain_disabled: - del self.fields["is_active"] - self.fields["role"].initial = account.group - if not account.is_local \ - and parameters.get_admin( - "LDAP_AUTH_METHOD", app="core") == "directbind": - del self.fields["password1"] - del self.fields["password2"] - - def domain_is_disabled(self): - """Little shortcut to get the domain's state. - - We need this information inside a template and the form is the - only object available... - - """ - if not self.instance.mailbox_set.count(): - return False - return not self.instance.mailbox_set.all()[0].domain.enabled - - def clean_role(self): - if self.user.group == "DomainAdmins": - if self.instance == self.user: - return "DomainAdmins" - return "SimpleUsers" - return self.cleaned_data["role"] - - def clean_username(self): - from django.core.validators import validate_email - if "role" not in self.cleaned_data: - return self.cleaned_data["username"] - if self.cleaned_data["role"] != "SimpleUsers": - return self.cleaned_data["username"] - uname = self.cleaned_data["username"].lower() - validate_email(uname) - return uname - - def clean_password2(self): - password1 = self.cleaned_data.get("password1", "") - password2 = self.cleaned_data["password2"] - if password1 != password2: - raise forms.ValidationError( - _("The two password fields didn't match.")) - return password2 - - def save(self, commit=True): - account = super(AccountFormGeneral, self).save(commit=False) - if self.user == account and not self.cleaned_data["is_active"]: - raise PermDeniedException(_("You can't disable your own account")) - if commit: - if "password1" in self.cleaned_data \ - and self.cleaned_data["password1"] != "": - account.set_password(self.cleaned_data["password1"]) - account.save() - account.set_role(self.cleaned_data["role"]) - return account - - -class AccountFormMail(forms.Form, DynamicForm): - - """Form to handle mail part.""" - - email = forms.EmailField(label=ugettext_lazy("E-mail"), required=False) - quota = forms.IntegerField( - label=ugettext_lazy("Quota"), - required=False, - help_text=_("Quota in MB for this mailbox. Define a custom value or " - "use domain's default one. Leave empty to define an " - "unlimited value (not allowed for domain " - "administrators)."), - widget=forms.widgets.TextInput(attrs={"class": "form-control"}) - ) - quota_act = forms.BooleanField(required=False) - aliases = forms.EmailField( - label=ugettext_lazy("Alias(es)"), - required=False, - help_text=ugettext_lazy( - "Alias(es) of this mailbox. Indicate only one address per input, " - "press ENTER to add a new input. Use the '*' character to create " - "a 'catchall' alias (ex: *@domain.tld)." - ) - ) - - def __init__(self, *args, **kwargs): - if "instance" in kwargs: - self.mb = kwargs["instance"] - del kwargs["instance"] - else: - self.mb = None - super(AccountFormMail, self).__init__(*args, **kwargs) - self.field_widths = { - "quota": 3 - } - self.extra_fields = [] - result = events.raiseQueryEvent('ExtraFormFields', 'mailform', self.mb) - for fname, field in result: - self.fields[fname] = field - self.extra_fields.append(fname) - if self.mb is not None: - self.fields["email"].required = True - cpt = 1 - for alias in self.mb.alias_set.all(): - if len(alias.get_recipients()) >= 2: - continue - name = "aliases_%d" % cpt - self._create_field(forms.EmailField, name, alias.full_address) - cpt += 1 - self.fields["email"].initial = self.mb.full_address - self.fields["quota_act"].initial = self.mb.use_domain_quota - if not self.mb.use_domain_quota and self.mb.quota: - self.fields["quota"].initial = self.mb.quota - else: - self.fields["quota_act"].initial = True - - if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "aliases", forms.EmailField) - - def clean_email(self): - """Ensure lower case emails""" - return self.cleaned_data["email"].lower() - - def clean(self): - """Custom fields validation. - - Check if quota is >= 0 only when the domain value is not used. - """ - super(AccountFormMail, self).clean() - if not self.cleaned_data["quota_act"] \ - and self.cleaned_data['quota'] is not None: - if self.cleaned_data["quota"] < 0: - self.add_error("quota", _("Must be a positive integer")) - return self.cleaned_data - - def create_mailbox(self, user, account): - """Create a mailbox associated to :kw:`account`.""" - locpart, domname = split_mailbox(self.cleaned_data["email"]) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise NotFound(_("Domain does not exist")) - if not user.can_access(domain): - raise PermDeniedException - try: - Mailbox.objects.get(address=locpart, domain=domain) - except Mailbox.DoesNotExist: - pass - else: - raise Conflict( - _("Mailbox %s already exists" % self.cleaned_data["email"]) - ) - events.raiseEvent("CanCreate", user, "mailboxes") - self.mb = Mailbox(address=locpart, domain=domain, user=account, - use_domain_quota=self.cleaned_data["quota_act"]) - self.mb.set_quota(self.cleaned_data["quota"], - user.has_perm("admin.add_domain")) - self.mb.save(creator=user) - - def update_mailbox(self, user, account): - newaddress = None - if self.cleaned_data["email"] != self.mb.full_address: - newaddress = self.cleaned_data["email"] - elif (account.group == "SimpleUsers" and - account.username != self.mb.full_address): - newaddress = account.username - if newaddress is not None: - self.mb.old_full_address = self.mb.full_address - local_part, domname = split_mailbox(newaddress) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise NotFound(_("Domain does not exist")) - if not user.can_access(domain): - raise PermDeniedException - self.mb.rename(local_part, domain) - - self.mb.use_domain_quota = self.cleaned_data["quota_act"] - override_rules = True \ - if not self.mb.quota or user.has_perm("admin.add_domain") \ - else False - self.mb.set_quota(self.cleaned_data["quota"], override_rules) - self.mb.save() - events.raiseEvent('MailboxModified', self.mb) - - def _update_aliases(self, user, account): - """Update mailbox aliases.""" - aliases = [] - for name, value in self.cleaned_data.iteritems(): - if not name.startswith("aliases"): - continue - if value == "": - continue - aliases.append(value.lower()) - - for alias in self.mb.alias_set.all(): - if alias.full_address not in aliases: - if len(alias.get_recipients()) >= 2: - continue - alias.delete() - else: - aliases.remove(alias.full_address) - if not aliases: - return - events.raiseEvent( - "CanCreate", user, "mailbox_aliases", len(aliases) - ) - for alias in aliases: - local_part, domname = split_mailbox(alias) - try: - self.mb.alias_set.get(address=local_part, domain__name=domname) - except Alias.DoesNotExist: - pass - else: - continue - al = Alias(address=local_part, enabled=account.is_active) - al.domain = Domain.objects.get(name=domname) - al.save(int_rcpts=[self.mb]) - al.post_create(user) - - def save(self, user, account): - """Save or update account mailbox.""" - if self.cleaned_data["email"] == "": - return None - - if self.cleaned_data["quota_act"]: - self.cleaned_data["quota"] = None - - if not hasattr(self, "mb") or self.mb is None: - self.create_mailbox(user, account) - else: - self.update_mailbox(user, account) - events.raiseEvent( - 'SaveExtraFormFields', 'mailform', self.mb, self.cleaned_data - ) - - account.email = self.cleaned_data["email"] - account.save() - - self._update_aliases(user, account) - - return self.mb - - -class AccountPermissionsForm(forms.Form, DynamicForm): - domains = DomainNameField( - label=ugettext_lazy("Domain(s)"), - required=False, - help_text=ugettext_lazy("Domain(s) that user administrates") - ) - - def __init__(self, *args, **kwargs): - if "instance" in kwargs: - self.account = kwargs["instance"] - del kwargs["instance"] - - super(AccountPermissionsForm, self).__init__(*args, **kwargs) - - if not hasattr(self, "account") or self.account is None: - return - for pos, dom in enumerate(Domain.objects.get_for_admin(self.account)): - name = "domains_%d" % (pos + 1) - self._create_field(DomainNameField, name, dom.name) - if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "domains", DomainNameField) - - def save(self): - current_domains = [ - dom.name for dom in Domain.objects.get_for_admin(self.account) - ] - for name, value in self.cleaned_data.items(): - if not name.startswith("domains"): - continue - if value in ["", None]: - continue - if value not in current_domains: - domain = Domain.objects.get(name=value) - domain.add_admin(self.account) - - for domain in Domain.objects.get_for_admin(self.account): - if not filter(lambda name: self.cleaned_data[name] == domain.name, - self.cleaned_data.keys()): - domain.remove_admin(self.account) - - -class AccountForm(TabForms): - - """Account edition form.""" - - def __init__(self, request, *args, **kwargs): - self.user = request.user - self.forms = [ - dict(id="general", title=_("General"), - formtpl="admin/account_general_form.html", - cls=AccountFormGeneral, - new_args=[self.user], mandatory=True), - dict(id="mail", title=_("Mail"), formtpl="admin/mailform.html", - cls=AccountFormMail), - dict( - id="perms", title=_("Permissions"), - formtpl="admin/permsform.html", - cls=AccountPermissionsForm - ) - ] - cbargs = [self.user] - if "instances" in kwargs: - cbargs += [kwargs["instances"]["general"]] - self.forms += events.raiseQueryEvent("ExtraAccountForm", *cbargs) - - super(AccountForm, self).__init__(request, *args, **kwargs) - - def extra_context(self, context): - account = self.instances["general"] - context.update({ - 'title': account.username, - 'formid': 'accountform', - 'action': reverse("admin:account_change", args=[account.id]), - }) - - def check_perms(self, account): - if account.is_superuser: - return False - return self.user.has_perm("admin.add_domain") \ - and account.has_perm("core.add_user") - - def _before_is_valid(self, form): - if form["id"] == "general": - return True - - if hasattr(self, "check_%s" % form["id"]): - if not getattr(self, "check_%s" % form["id"])(self.account): - return False - return True - - extra_forms = events.raiseQueryEvent( - "CheckExtraAccountForm", self.account, form) - if False in extra_forms: - return False - return True - - def is_valid(self): - """Two steps validation. - """ - self.instances["general"].oldgroup = self.instances["general"].group - if super(AccountForm, self).is_valid(mandatory_only=True): - self.account = self.forms[0]["instance"].save() - return super(AccountForm, self).is_valid(optional_only=True) - return False - - def save(self): - """Custom save method - - As forms interact with each other, it is simpler to make - custom code to save them. - """ - events.raiseEvent( - "AccountModified", self.instances["general"], self.account - ) - self.forms[1]["instance"].save(self.user, self.account) - if len(self.forms) <= 2: - return - for f in self.forms[2:]: - f["instance"].save() - - def done(self): - return render_to_json_response(_("Account updated")) - - -class AccountWizard(WizardForm): - - """Account creation wizard. - """ - - def __init__(self, request): - super(AccountWizard, self).__init__(request) - self.add_step( - AccountFormGeneral, _("General"), new_args=[request.user] - ) - self.add_step( - AccountFormMail, _("Mail"), formtpl="admin/mailform.html" - ) - - def extra_context(self, context): - context.update({ - 'title': _("New account"), - 'action': reverse("admin:account_add"), - 'formid': 'newaccount_form' - }) - - def done(self): - from modoboa.lib.web_utils import render_to_json_response - - account = self.first_step.form.save() - account.post_create(self.request.user) - mailform = self.steps[1].form - mailform.save(self.request.user, account) - return render_to_json_response(_("Account created")) diff --git a/modoboa/extensions/admin/forms/alias.py b/modoboa/extensions/admin/forms/alias.py deleted file mode 100644 index 12485eebc..000000000 --- a/modoboa/extensions/admin/forms/alias.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Forms related to aliases management.""" - -from collections import OrderedDict - -from django import forms -from django.http import QueryDict -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.extensions.admin.models import ( - Domain, Mailbox, Alias -) -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import BadRequest, NotFound, Conflict -from modoboa.lib.form_utils import ( - DynamicForm -) - - -class AliasForm(forms.ModelForm, DynamicForm): - email = forms.EmailField( - label=ugettext_lazy("Email address"), - help_text=ugettext_lazy( - "The distribution list address. Use the '*' character to create a " - "'catchall' address (ex: *@domain.tld)." - ), - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - recipients = forms.EmailField( - label=ugettext_lazy("Recipients"), required=False, - help_text=ugettext_lazy( - "Mailbox(es) this alias will point to. Indicate only one address " - "per input, press ENTER to add a new input." - ), - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - - class Meta: - model = Alias - fields = ("enabled",) - - def __init__(self, user, *args, **kwargs): - self.user = user - super(AliasForm, self).__init__(*args, **kwargs) - self.fields = OrderedDict( - (key, self.fields[key]) for key in - ['email', 'recipients', 'enabled'] - ) - - if len(args) and isinstance(args[0], QueryDict): - if "instance" in kwargs: - if not kwargs["instance"].domain.enabled: - del self.fields["enabled"] - self._load_from_qdict(args[0], "recipients", forms.EmailField) - elif "instance" in kwargs: - dlist = kwargs["instance"] - self.fields["email"].initial = dlist.full_address - if not dlist.domain.enabled: - self.fields["enabled"].widget.attrs["disabled"] = "disabled" - cpt = 1 - for al in dlist.aliases.all(): - name = "recipients_%d" % cpt - self._create_field(forms.EmailField, name, al.full_address, 2) - cpt += 1 - for mb in dlist.mboxes.all(): - name = "recipients_%d" % (cpt) - self._create_field(forms.EmailField, name, mb.full_address, 2) - cpt += 1 - for addr in dlist.extmboxes.split(','): - if addr == "": - continue - name = "recipients_%d" % (cpt) - self._create_field(forms.EmailField, name, addr, 2) - cpt += 1 - - def clean_email(self): - localpart, domname = split_mailbox(self.cleaned_data["email"]) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise forms.ValidationError(_("Domain does not exist")) - if not self.user.can_access(domain): - raise forms.ValidationError( - _("You don't have access to this domain") - ) - return self.cleaned_data["email"].lower() - - def set_recipients(self): - """Recipients dispatching - - We make a difference between 'local' recipients (the ones hosted - by Modoboa) and 'external' recipients. - """ - self.ext_rcpts = [] - self.int_rcpts = [] - total = 0 - - for k, v in self.cleaned_data.items(): - if not k.startswith("recipients"): - continue - if v == "": - continue - local_part, domname = split_mailbox(v) - if domname is None: - raise BadRequest( - u"%s %s" % (_("Invalid mailbox"), v) - ) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - domain = None - if domain is not None: - try: - rcpt = Alias.objects.get(domain=domain, address=local_part) - if rcpt.full_address == self.cleaned_data["email"]: - rcpt = None - except Alias.DoesNotExist: - rcpt = None - if rcpt is None: - try: - rcpt = Mailbox.objects.get( - domain=domain, address=local_part) - except Mailbox.DoesNotExist: - raise NotFound( - _("Local recipient %s not found" % v) - ) - if rcpt in self.int_rcpts: - raise Conflict( - _("Recipient %s already present" % v) - ) - self.int_rcpts += [rcpt] - total += 1 - continue - - if v in self.ext_rcpts: - raise Conflict( - _("Recipient %s already present" % v) - ) - self.ext_rcpts += [v] - total += 1 - - if total == 0: - raise BadRequest(_("No recipient defined")) - - def save(self, commit=True): - alias = super(AliasForm, self).save(commit=False) - localpart, domname = split_mailbox(self.cleaned_data["email"]) - alias.address = localpart - alias.domain = Domain.objects.get(name=domname) - if commit: - alias.save(int_rcpts=self.int_rcpts, ext_rcpts=self.ext_rcpts) - self.save_m2m() - return alias diff --git a/modoboa/extensions/admin/forms/domain.py b/modoboa/extensions/admin/forms/domain.py deleted file mode 100644 index 8fc8afb15..000000000 --- a/modoboa/extensions/admin/forms/domain.py +++ /dev/null @@ -1,362 +0,0 @@ -"""Forms related to domains management.""" - -from django import forms -from django.core.urlresolvers import reverse -from django.http import QueryDict -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.core.models import User -from modoboa.extensions.admin.lib import check_if_domain_exists -from modoboa.extensions.admin.models import ( - Domain, DomainAlias, Mailbox, Alias, Quota -) -from modoboa.lib import events, parameters -from modoboa.lib.exceptions import ModoboaException, Conflict -from modoboa.lib.form_utils import ( - DomainNameField, YesNoField, WizardForm, DynamicForm, TabForms -) -from modoboa.lib.web_utils import render_to_json_response - - -class DomainFormGeneral(forms.ModelForm, DynamicForm): - quota = forms.IntegerField( - label=ugettext_lazy("Quota"), - required=False, - help_text=ugettext_lazy( - "Default quota in MB applied to mailboxes. Leave empty to use the " - "default value." - ) - ) - aliases = DomainNameField( - label=ugettext_lazy("Alias(es)"), - required=False, - help_text=ugettext_lazy( - "Alias(es) of this domain. Indicate only one name per input, " - "press ENTER to add a new input." - ) - ) - name = DomainNameField( - widget=forms.TextInput() - ) - - class Meta: - model = Domain - fields = ("name", "quota", "aliases", "enabled") - - def __init__(self, *args, **kwargs): - self.oldname = None - if "instance" in kwargs: - self.oldname = kwargs["instance"].name - super(DomainFormGeneral, self).__init__(*args, **kwargs) - - self.field_widths = { - "quota": 3 - } - - if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "aliases", DomainNameField) - elif "instance" in kwargs: - d = kwargs["instance"] - for pos, dalias in enumerate(d.domainalias_set.all()): - name = "aliases_%d" % (pos + 1) - self._create_field(forms.CharField, name, dalias.name, 3) - - def clean_quota(self): - if self.cleaned_data['quota'] is None: - return int(parameters.get_admin('DEFAULT_DOMAIN_QUOTA')) - if self.cleaned_data['quota'] < 0: - raise forms.ValidationError(_("Must be a positive integer")) - return self.cleaned_data['quota'] - - def clean(self): - """Custom fields validation. - - We want to prevent duplicate names between domains and domain - aliases. Extensions have the possibility to declare other - objects (see *CheckDomainName* event). - - The validation way is not very smart... - """ - super(DomainFormGeneral, self).clean() - cleaned_data = self.cleaned_data - name = cleaned_data["name"] - label = check_if_domain_exists(name, [(DomainAlias, _('domain alias'))]) - if label is not None: - self.add_error( - "name", _("A %s with this name already exists") % unicode(label) - ) - for k in cleaned_data.keys(): - if not k.startswith("aliases"): - continue - if cleaned_data[k] == "": - del cleaned_data[k] - continue - if cleaned_data[k] == name: - self._errors[k] = self.error_class( - [_("A %s with this name already exists") % _("domain")] - ) - del cleaned_data[k] - continue - label = check_if_domain_exists( - cleaned_data[k], [(Domain, _("domain"))]) - if label is not None: - self.add_error( - k, _("A %s with this name already exists") % unicode(label)) - return cleaned_data - - def update_mailbox_quotas(self, domain): - """Update all quota records associated to this domain. - - This method must be called only when a domain gets renamed. As - the primary key used for a quota is an email address, rename a - domain will change all associated email addresses, so it will - change the primary keys used for quotas. The consequence is we - can't issue regular UPDATE queries using the .save() method of - a Quota instance (it will trigger an INSERT as the primary key - has changed). - - So, we use this ugly hack to bypass this behaviour. It is not - perfomant at all as it will generate one query per quota - record to update. - """ - for q in Quota.objects.filter(username__contains="@%s" % self.oldname): - username = q.username.replace( - '@%s' % self.oldname, '@%s' % domain.name) - newq = Quota.objects.create( - username=username, bytes=q.bytes, messages=q.messages) - q.delete() - - def save(self, user, commit=True, domalias_post_create=False): - """Custom save method - - Updating a domain may have consequences on other objects - (domain alias, mailbox, quota). The most tricky part concerns - quotas update. - - """ - d = super(DomainFormGeneral, self).save(commit=False) - if commit: - old_mail_homes = None - if self.oldname is not None and d.name != self.oldname: - d.name = self.oldname - old_mail_homes = \ - dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) - d.name = self.cleaned_data['name'] - d.save() - Mailbox.objects.filter(domain=d, use_domain_quota=True) \ - .update(quota=d.quota) - aliases = [] - for k, v in self.cleaned_data.iteritems(): - if not k.startswith("aliases"): - continue - if v in ["", None]: - continue - aliases.append(v) - for dalias in d.domainalias_set.all(): - if dalias.name not in aliases: - dalias.delete() - else: - aliases.remove(dalias.name) - if aliases: - events.raiseEvent( - "CanCreate", user, "domain_aliases", len(aliases)) - for alias in aliases: - try: - d.domainalias_set.get(name=alias) - except DomainAlias.DoesNotExist: - pass - else: - continue - al = DomainAlias(name=alias, target=d, enabled=d.enabled) - al.save(creator=user) if domalias_post_create else al.save() - - if old_mail_homes is not None: - self.update_mailbox_quotas(d) - for mb in d.mailbox_set.all(): - mb.rename_dir(old_mail_homes[mb.id]) - - return d - - -class DomainFormOptions(forms.Form): - create_dom_admin = YesNoField( - label=ugettext_lazy("Create a domain administrator"), - initial="no", - help_text=ugettext_lazy( - "Automatically create an administrator for this domain" - ) - ) - - dom_admin_username = forms.CharField( - label=ugettext_lazy("Name"), - initial="admin", - help_text=ugettext_lazy( - "The administrator's name. Don't include the domain's name here, " - "it will be automatically appended." - ), - required=False - ) - - create_aliases = YesNoField( - label=ugettext_lazy("Create aliases"), - initial="yes", - help_text=ugettext_lazy( - "Automatically create standard aliases for this domain" - ), - required=False - ) - - def __init__(self, user, *args, **kwargs): - super(DomainFormOptions, self).__init__(*args, **kwargs) - result = events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins') - if False in result: - self.fields = {} - return - if args: - if args[0].get("create_dom_admin", "no") == "yes": - self.fields["dom_admin_username"].required = True - self.fields["create_aliases"].required = True - - def clean_dom_admin_username(self): - """Ensure admin username is an email address.""" - if '@' in self.cleaned_data["dom_admin_username"]: - raise forms.ValidationError(_("Invalid format")) - return self.cleaned_data["dom_admin_username"] - - def save(self, user, domain): - if not self.fields: - return - if self.cleaned_data["create_dom_admin"] == "no": - return - username = "%s@%s" % ( - self.cleaned_data["dom_admin_username"], domain.name) - try: - da = User.objects.get(username=username) - except User.DoesNotExist: - pass - else: - raise Conflict(_("User '%s' already exists" % username)) - events.raiseEvent("CanCreate", user, "mailboxes") - da = User(username=username, email=username, is_active=True) - da.set_password("password") - da.save() - da.set_role("DomainAdmins") - da.post_create(user) - mb = Mailbox( - address=self.cleaned_data["dom_admin_username"], domain=domain, - user=da, use_domain_quota=True - ) - mb.set_quota(override_rules=user.has_perm("admin.change_domain")) - mb.save(creator=user) - - if self.cleaned_data["create_aliases"] == "yes": - events.raiseEvent("CanCreate", user, "mailbox_aliases") - alias = Alias(address="postmaster", domain=domain, enabled=True) - alias.save(int_rcpts=[mb]) - alias.post_create(user) - - domain.add_admin(da) - - -class DomainForm(TabForms): - - """Domain edition form.""" - - template_name = "admin/editdomainform.html" - - def __init__(self, request, *args, **kwargs): - self.user = request.user - self.forms = [] - if self.user.has_perm("admin.change_domain"): - self.forms.append({ - "id": "general", - "title": _("General"), - "formtpl": "admin/domain_general_form.html", - "cls": DomainFormGeneral, - "mandatory": True - }) - - cbargs = [self.user] - if "instances" in kwargs: - cbargs += [kwargs["instances"]["general"]] - self.forms += events.raiseQueryEvent("ExtraDomainForm", *cbargs) - if not self.forms: - self.active_id = "admins" - super(DomainForm, self).__init__(request, *args, **kwargs) - - def extra_context(self, context): - domain = self.instances["general"] - domadmins = [u for u in domain.admins - if self.request.user.can_access(u) and not u.is_superuser] - if not self.request.user.is_superuser: - domadmins = [u for u in domadmins if u.group == "DomainAdmins"] - context.update({ - "title": domain.name, - "action": reverse("admin:domain_change", args=[domain.pk]), - "formid": "domform", - "domain": domain, - "domadmins": domadmins - }) - - def is_valid(self): - """Custom validation. - - We just save the current name before it is potentially - modified. - - """ - self.instances["general"].oldname = self.instances["general"].name - return super(DomainForm, self).is_valid() - - def save(self): - """Custom save method. - - As forms interact with each other, it is easier to make custom - code to save them. - """ - self.forms[0]['instance'].save( - self.request.user, domalias_post_create=True - ) - for f in self.forms[1:]: - f["instance"].save(self.request.user) - - def done(self): - events.raiseEvent("DomainModified", self.instances["general"]) - return render_to_json_response(_("Domain modified")) - - -class DomainWizard(WizardForm): - - """Domain creation wizard. - """ - - def __init__(self, request): - super(DomainWizard, self).__init__(request) - self.add_step( - DomainFormGeneral, _("General"), - formtpl="admin/domain_general_form.html" - ) - self.add_step( - DomainFormOptions, _("Options"), - formtpl="admin/domain_options_form.html", - new_args=[self.request.user] - ) - - def extra_context(self, context): - context.update({ - "title": _("New domain"), - "action": reverse("admin:domain_add"), - "formid": "domform" - }) - - def done(self): - genform = self.first_step.form - domain = genform.save(self.request.user) - domain.post_create(self.request.user) - try: - self.steps[1].form.save(self.request.user, domain) - except ModoboaException: - from django.db import transaction - transaction.rollback() - raise - return render_to_json_response(_("Domain created")) diff --git a/modoboa/extensions/admin/forms/export.py b/modoboa/extensions/admin/forms/export.py deleted file mode 100644 index f1f8a868b..000000000 --- a/modoboa/extensions/admin/forms/export.py +++ /dev/null @@ -1,45 +0,0 @@ -from django import forms -from django.utils.translation import ugettext_lazy - - -class ExportDataForm(forms.Form): - filename = forms.CharField( - label=ugettext_lazy("File name"), - max_length=100, - required=False, - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - sepchar = forms.CharField( - label=ugettext_lazy("Separator"), - max_length=1, - required=False, - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - - def __init__(self, *args, **kwargs): - super(ExportDataForm, self).__init__(*args, **kwargs) - self.fields["sepchar"].widget.attrs = {"class": "col-md-1 form-control"} - - def clean_sepchar(self): - if self.cleaned_data["sepchar"] == "": - return ";" - return str(self.cleaned_data["sepchar"]) - - def clean_filename(self): - if self.cleaned_data["filename"] == "": - return self.fields["filename"].initial - return str(self.cleaned_data["filename"]) - - -class ExportDomainsForm(ExportDataForm): - - def __init__(self, *args, **kwargs): - super(ExportDomainsForm, self).__init__(*args, **kwargs) - self.fields["filename"].initial = "modoboa-domains.csv" - - -class ExportIdentitiesForm(ExportDataForm): - - def __init__(self, *args, **kwargs): - super(ExportIdentitiesForm, self).__init__(*args, **kwargs) - self.fields["filename"].initial = "modoboa-identities.csv" diff --git a/modoboa/extensions/admin/forms/forward.py b/modoboa/extensions/admin/forms/forward.py deleted file mode 100644 index c4d1e4a7b..000000000 --- a/modoboa/extensions/admin/forms/forward.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Forms related to forwards management.""" - -from django import forms -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.extensions.admin.models import ( - Domain -) -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import BadRequest, PermDeniedException - - -class ForwardForm(forms.Form): - dest = forms.CharField( - label=ugettext_lazy("Recipient(s)"), - widget=forms.Textarea(attrs={"class": "form-control"}), - required=False, - help_text=ugettext_lazy( - "Indicate one or more recipients separated by a ','") - ) - keepcopies = forms.BooleanField( - label=ugettext_lazy("Keep local copies"), - required=False, - help_text=ugettext_lazy( - "Forward messages and store copies into your local mailbox") - ) - - def parse_dest(self): - self.dests = [] - rawdata = self.cleaned_data["dest"].strip() - if rawdata == "": - return - for d in rawdata.split(","): - local_part, domname = split_mailbox(d) - if not local_part or not domname or not len(domname): - raise BadRequest("Invalid mailbox syntax for %s" % d) - try: - Domain.objects.get(name=domname) - except Domain.DoesNotExist: - self.dests += [d] - else: - raise PermDeniedException( - _("You can't define a forward to a local destination. " - "Please ask your administrator to create an alias " - "instead.") - ) diff --git a/modoboa/extensions/admin/forms/import_.py b/modoboa/extensions/admin/forms/import_.py deleted file mode 100644 index 0091e4ba8..000000000 --- a/modoboa/extensions/admin/forms/import_.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Forms related to import operations.""" - -from django import forms -from django.utils.translation import ugettext_lazy - - -class ImportDataForm(forms.Form): - - """Base form to import objects.""" - - sourcefile = forms.FileField(label=ugettext_lazy("Select a file")) - sepchar = forms.CharField( - label=ugettext_lazy("Separator"), - max_length=1, - required=False, - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - continue_if_exists = forms.BooleanField( - label=ugettext_lazy("Continue on error"), required=False, - help_text=ugettext_lazy("Don't treat duplicated objects as error") - ) - - def __init__(self, *args, **kwargs): - super(ImportDataForm, self).__init__(*args, **kwargs) - self.fields["sepchar"].widget.attrs = {"class": "col-md-1 form-control"} - - def clean_sepchar(self): - if self.cleaned_data["sepchar"] == "": - return ";" - return str(self.cleaned_data["sepchar"]) - - -class ImportIdentitiesForm(ImportDataForm): - - """A form to import identities.""" - - crypt_password = forms.BooleanField( - label=ugettext_lazy("Crypt passwords"), required=False, - help_text=ugettext_lazy( - "Check this option if passwords contained in your file " - "are not crypted" - ) - ) diff --git a/modoboa/extensions/admin/lib.py b/modoboa/extensions/admin/lib.py deleted file mode 100644 index 9ba1e9585..000000000 --- a/modoboa/extensions/admin/lib.py +++ /dev/null @@ -1,114 +0,0 @@ -# coding: utf-8 -from functools import wraps -from itertools import chain -from django.db.models import Q -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import ugettext as _ -from modoboa.core.models import User -from modoboa.lib import events -from modoboa.lib.exceptions import PermDeniedException -from modoboa.lib.email_utils import split_mailbox -from modoboa.extensions.admin.models import ( - Domain, Alias -) - - -def needs_mailbox(): - """Check if the current user owns at least one mailbox - - Some applications (the webmail for example) need a mailbox to - work. - """ - def decorator(f): - @wraps(f) - def wrapped_f(request, *args, **kwargs): - if request.user.mailbox_set.count(): - return f(request, *args, **kwargs) - raise PermDeniedException(_("A mailbox is required")) - return wrapped_f - return decorator - - -def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None): - """Return all the identities owned by a user. - - :param user: the desired user - :param str searchquery: search pattern - :param list idtfilter: identity type filters - :param list grpfilter: group names filters - :return: a queryset - """ - accounts = [] - if idtfilter is None or not idtfilter or idtfilter == "account": - ids = user.objectaccess_set \ - .filter(content_type=ContentType.objects.get_for_model(user)) \ - .values_list('object_id', flat=True) - q = Q(pk__in=ids) - if searchquery is not None: - q &= Q(username__icontains=searchquery) \ - | Q(email__icontains=searchquery) - if grpfilter is not None and grpfilter: - if grpfilter == "SuperAdmins": - q &= Q(is_superuser=True) - else: - q &= Q(groups__name=grpfilter) - accounts = User.objects.select_related().filter(q) - - aliases = [] - if idtfilter is None or not idtfilter \ - or (idtfilter in ["alias", "forward", "dlist"]): - alct = ContentType.objects.get_for_model(Alias) - ids = user.objectaccess_set.filter(content_type=alct) \ - .values_list('object_id', flat=True) - q = Q(pk__in=ids) - if searchquery is not None: - if '@' in searchquery: - local_part, domname = split_mailbox(searchquery) - if local_part: - q &= Q(address__icontains=local_part) - if domname: - q &= Q(domain__name__icontains=domname) - else: - q &= Q(address__icontains=searchquery) | \ - Q(domain__name__icontains=searchquery) - aliases = Alias.objects.select_related().filter(q) - if idtfilter is not None and idtfilter: - aliases = [al for al in aliases if al.type == idtfilter] - return chain(accounts, aliases) - - -def get_domains(user, domfilter=None, searchquery=None, **extrafilters): - """Return all the domains the user can access. - - :param ``User`` user: user object - :param str searchquery: filter - :rtype: list - :return: a list of domains and/or relay domains - """ - domains = [] - if domfilter is None or not domfilter or domfilter == 'domain': - domains = Domain.objects.get_for_admin(user) - if searchquery is not None: - q = Q(name__contains=searchquery) - q |= Q(domainalias__name__contains=searchquery) - domains = domains.filter(q).distinct() - extra_domain_entries = events.raiseQueryEvent( - 'ExtraDomainEntries', user, domfilter, searchquery, **extrafilters - ) - return chain(domains, extra_domain_entries) - - -def check_if_domain_exists(name, extra_checks=None): - """Check if a domain already exists. - - We not only look for domains, we also look for every object that - could conflict with a domain (domain alias, etc.) - - """ - dtypes = events.raiseQueryEvent('CheckDomainName') - if extra_checks is not None: - dtypes = extra_checks + dtypes - for dtype, label in dtypes: - if dtype.objects.filter(name=name).exists(): - return label - return None diff --git a/modoboa/extensions/admin/management/__init__.py b/modoboa/extensions/admin/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/admin/management/commands/__init__.py b/modoboa/extensions/admin/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/admin/management/commands/handle_mailbox_operations.py b/modoboa/extensions/admin/management/commands/handle_mailbox_operations.py deleted file mode 100644 index b0cea9d46..000000000 --- a/modoboa/extensions/admin/management/commands/handle_mailbox_operations.py +++ /dev/null @@ -1,97 +0,0 @@ - -import logging -from optparse import make_option -import os - -from django.core.management.base import BaseCommand - -from modoboa.core.management.commands import CloseConnectionMixin -from modoboa.lib import parameters -from modoboa.lib.sysutils import exec_cmd -from modoboa.lib.exceptions import InternalError -from modoboa.extensions.admin.models import MailboxOperation - - -class OperationError(Exception): - pass - - -class Command(BaseCommand, CloseConnectionMixin): - help = 'Handles rename and delete operations on mailboxes' - - option_list = BaseCommand.option_list + ( - make_option( - '--pidfile', default='/tmp/handle_mailbox_operations.pid', - help='Path to the file that will contain the PID of this process' - ), - ) - - def __init__(self, *args, **kwargs): - super(Command, self).__init__(*args, **kwargs) - self.logger = logging.getLogger('modoboa.admin') - - def rename_mailbox(self, operation): - if not os.path.exists(operation.argument): - return - new_mail_home = operation.mailbox.mail_home - dirname = os.path.dirname(new_mail_home) - if not os.path.exists(dirname): - try: - os.makedirs(dirname) - except os.error as e: - raise OperationError(str(e)) - code, output = exec_cmd( - "mv %s %s" % (operation.argument, new_mail_home) - ) - if code: - raise OperationError(output) - - def delete_mailbox(self, operation): - if not os.path.exists(operation.argument): - return - code, output = exec_cmd( - "rm -r %s" % operation.argument - ) - if code: - raise OperationError(output) - - def check_pidfile(self, path): - """Check if this command is already running - - :param str path: path to the file containing the PID - :return: a boolean, True means we can go further - """ - if os.path.exists(path): - with open(path) as fp: - pid = fp.read().strip() - code, output = exec_cmd( - "grep handle_mailbox_operations /proc/%s/cmdline" % pid - ) - if not code: - return False - with open(path, 'w') as fp: - print >> fp, os.getpid() - return True - - def handle(self, *args, **options): - try: - if parameters.get_admin("HANDLE_MAILBOXES") == 'no': - return - except parameters.NotDefined: - return - if not self.check_pidfile(options['pidfile']): - return - for ope in MailboxOperation.objects.all(): - try: - f = getattr(self, '%s_mailbox' % ope.type) - except AttributeError: - continue - try: - f(ope) - except (OperationError, InternalError) as e: - self.logger.critical('%s failed (reason: %s)', - ope, str(e).encode('utf-8')) - else: - self.logger.info('%s succeed', ope) - ope.delete() - os.unlink(options['pidfile']) diff --git a/modoboa/extensions/admin/migrations/0001_initial.py b/modoboa/extensions/admin/migrations/0001_initial.py deleted file mode 100644 index ed6048142..000000000 --- a/modoboa/extensions/admin/migrations/0001_initial.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Alias', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('address', models.CharField(help_text="The alias address (without the domain part). For a 'catch-all' address, just enter an * character.", max_length=254, verbose_name='address')), - ('extmboxes', models.TextField(blank=True)), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')), - ('aliases', models.ManyToManyField(help_text='The aliases this alias points to', to='admin.Alias')), - ], - options={ - 'ordering': ['domain__name', 'address'], - 'permissions': (('view_aliases', 'View aliases'),), - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Domain', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The domain name', unique=True, max_length=100, verbose_name='name')), - ('quota', models.IntegerField()), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this domain', verbose_name='enabled')), - ], - options={ - 'ordering': ['name'], - 'permissions': (('view_domain', 'View domain'), ('view_domains', 'View domains')), - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='DomainAlias', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The alias name', unique=True, max_length=100, verbose_name='name')), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')), - ], - options={ - 'permissions': (('view_domaliases', 'View domain aliases'),), - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Mailbox', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('address', models.CharField(help_text='Mailbox address (without the @domain.tld part)', max_length=252, verbose_name='address')), - ('quota', models.PositiveIntegerField()), - ('use_domain_quota', models.BooleanField(default=False)), - ], - options={ - 'permissions': (('view_mailboxes', 'View mailboxes'),), - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='MailboxOperation', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('type', models.CharField(max_length=20, choices=[(b'rename', b'rename'), (b'delete', b'delete')])), - ('argument', models.TextField()), - ('mailbox', models.ForeignKey(blank=True, to='admin.Mailbox', null=True)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='ObjectDates', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('creation', models.DateTimeField(auto_now_add=True)), - ('last_modification', models.DateTimeField(auto_now=True)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Quota', - fields=[ - ('username', models.EmailField(max_length=254, serialize=False, primary_key=True)), - ('bytes', models.BigIntegerField(default=0)), - ('messages', models.IntegerField(default=0)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.AddField( - model_name='mailbox', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates'), - preserve_default=True, - ), - migrations.AddField( - model_name='mailbox', - name='domain', - field=models.ForeignKey(to='admin.Domain'), - preserve_default=True, - ), - migrations.AddField( - model_name='mailbox', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), - preserve_default=True, - ), - migrations.AddField( - model_name='domainalias', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates'), - preserve_default=True, - ), - migrations.AddField( - model_name='domainalias', - name='target', - field=models.ForeignKey(verbose_name='target', to='admin.Domain', help_text='The domain this alias points to'), - preserve_default=True, - ), - migrations.AddField( - model_name='domain', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates'), - preserve_default=True, - ), - migrations.AddField( - model_name='alias', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates'), - preserve_default=True, - ), - migrations.AddField( - model_name='alias', - name='domain', - field=models.ForeignKey(to='admin.Domain'), - preserve_default=True, - ), - migrations.AddField( - model_name='alias', - name='mboxes', - field=models.ManyToManyField(help_text='The mailboxes this alias points to', to='admin.Mailbox', verbose_name='mailboxes'), - preserve_default=True, - ), - migrations.AlterUniqueTogether( - name='alias', - unique_together=set([('address', 'domain')]), - ), - ] diff --git a/modoboa/extensions/admin/migrations/__init__.py b/modoboa/extensions/admin/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/admin/models/__init__.py b/modoboa/extensions/admin/models/__init__.py deleted file mode 100644 index 641eff9ad..000000000 --- a/modoboa/extensions/admin/models/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .base import AdminObject -from .domain import Domain -from .domain_alias import DomainAlias -from .mailbox import Mailbox, Quota, MailboxOperation -from .alias import Alias - -__all__ = [ - 'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', - 'MailboxOperation', 'AdminObject' -] diff --git a/modoboa/extensions/admin/models/alias.py b/modoboa/extensions/admin/models/alias.py deleted file mode 100644 index 7e1fb4a52..000000000 --- a/modoboa/extensions/admin/models/alias.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Models related to aliases management.""" - -from django.db import models -from django.utils.translation import ugettext as _, ugettext_lazy - -import reversion - -from .base import AdminObject -from .domain import Domain -from .mailbox import Mailbox -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import ( - PermDeniedException, BadRequest, Conflict -) - - -class Alias(AdminObject): - - """ - Mailbox alias. - """ - address = models.CharField( - ugettext_lazy('address'), max_length=254, - help_text=ugettext_lazy( - "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=ugettext_lazy('mailboxes'), - help_text=ugettext_lazy("The mailboxes this alias points to") - ) - aliases = models.ManyToManyField( - 'Alias', - help_text=ugettext_lazy("The aliases this alias points to") - ) - extmboxes = models.TextField(blank=True) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this alias"), - default=True - ) - _objectname = 'MailboxAlias' - - class Meta: - permissions = ( - ("view_aliases", "View aliases"), - ) - unique_together = (("address", "domain"),) - ordering = ["domain__name", "address"] - app_label = 'admin' - - def __unicode__(self): - return self.full_address - - @property - def full_address(self): - return "%s@%s" % (self.address, self.domain.name) - - @property - def identity(self): - return self.full_address - - @property - def name_or_rcpt(self): - rcpts_count = self.get_recipients_count() - if not rcpts_count: - return "---" - rcpts = self.get_recipients() - if rcpts_count > 1: - return "%s, ..." % rcpts[0] - return rcpts[0] - - @property - def type(self): - cpt = self.get_recipients_count() - if cpt > 1: - return "dlist" - if self.extmboxes != "": - return "forward" - return "alias" - - @property - def tags(self): - labels = { - "dlist": _("distribution list"), - "forward": _("forward"), - "alias": _("alias") - } - altype = self.type - return [{"name": altype, "label": labels[altype], "type": "idt"}] - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - super(Alias, self).post_create(creator) - if creator.is_superuser: - for admin in self.domain.admins: - grant_access_to_object(admin, self) - - def save(self, *args, **kwargs): - """Custom save method.""" - if 'ext_rcpts' in kwargs: - self.extmboxes = ",".join(kwargs['ext_rcpts']) \ - if kwargs['ext_rcpts'] else "" - del kwargs['ext_rcpts'] - if 'int_rcpts' in kwargs: - int_rcpts = kwargs['int_rcpts'] - del kwargs['int_rcpts'] - else: - int_rcpts = [] - super(Alias, self).save(*args, **kwargs) - curaliases = self.aliases.all() - curmboxes = self.mboxes.all() - for rcpt in int_rcpts: - if isinstance(rcpt, Alias): - if rcpt not in curaliases: - self.aliases.add(rcpt) - continue - if rcpt not in curmboxes: - self.mboxes.add(rcpt) - for rcpt in curaliases: - if rcpt not in int_rcpts: - self.aliases.remove(rcpt) - for rcpt in curmboxes: - if rcpt not in int_rcpts: - self.mboxes.remove(rcpt) - - def get_recipients(self, with_external=True): - """Return the recipients list. - - Internal and external addresses are mixed into a single list. - - :param bool with_external: include external addresses or not - :rtype: list - :return: a list of addresses - """ - result = [al.full_address for al in self.aliases.all()] - result += [mb.full_address for mb in self.mboxes.all()] - if with_external and self.extmboxes != "": - result += self.extmboxes.split(',') - return result - - def get_recipients_count(self): - """Return the number of recipients of this alias. - - :rtype: int - """ - total = 0 - if self.extmboxes != "": - total += len(self.extmboxes.split(',')) - return total + self.aliases.count() + self.mboxes.count() - - def from_csv(self, user, row, expected_elements=5): - """Create a new alias from a CSV file entry - - """ - if len(row) < expected_elements: - raise BadRequest(_("Invalid line: %s" % row)) - localpart, domname = split_mailbox(row[1].strip()) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise BadRequest(_("Domain '%s' does not exist" % domname)) - if not user.can_access(domain): - raise PermDeniedException - try: - Alias.objects.get(address=localpart, domain__name=domain) - except Alias.DoesNotExist: - pass - else: - raise Conflict - self.address = localpart - self.domain = domain - self.enabled = (row[2].strip() in ["True", "1", "yes", "y"]) - int_rcpts = [] - ext_rcpts = [] - for rcpt in row[3:]: - rcpt = rcpt.strip() - if not rcpt: - continue - localpart, domname = split_mailbox(rcpt) - try: - Domain.objects.get(name=domname) - except Domain.DoesNotExist: - ext_rcpts += [rcpt] - continue - try: - target = Alias.objects.get( - domain__name=domname, address=localpart - ) - if target.full_address == self.full_address: - target = None - except Alias.DoesNotExist: - target = None - if target is None: - try: - target = Mailbox.objects.get(address=localpart, - domain__name=domname) - except Mailbox.DoesNotExist: - raise BadRequest(_("Local recipient %s not found" % rcpt)) - int_rcpts += [target] - self.save(int_rcpts=int_rcpts, ext_rcpts=ext_rcpts) - self.post_create(user) - - def to_csv(self, csvwriter): - row = [self.type, self.full_address.encode("utf-8"), self.enabled] - row += self.get_recipients() - csvwriter.writerow(row) - -reversion.register(Alias) diff --git a/modoboa/extensions/admin/models/base.py b/modoboa/extensions/admin/models/base.py deleted file mode 100644 index c576bdcad..000000000 --- a/modoboa/extensions/admin/models/base.py +++ /dev/null @@ -1,81 +0,0 @@ -from django.db import models -from modoboa.lib import events -from modoboa.lib.permissions import ( - grant_access_to_object, ungrant_access_to_object -) - - -class ObjectDates(models.Model): - - """Dates recording for admin objects - - This table keeps creation and last modification dates for Domains, - domain aliases, mailboxes and aliases objects. - """ - creation = models.DateTimeField(auto_now_add=True) - last_modification = models.DateTimeField(auto_now=True) - - class Meta: - app_label = 'admin' - - @staticmethod - def set_for_object(obj): - """Initialize or update dates for a given object. - - :param obj: an admin object (Domain, Mailbox, etc) - """ - try: - dates = getattr(obj, "dates") - except ObjectDates.DoesNotExist: - dates = ObjectDates() - dates.save() - obj.dates = dates - - -class AdminObject(models.Model): - - """Abstract model to support dates - - Inherit from this model to automatically add the "dates" feature - to another model. It defines the appropriate field and handles - saves. - """ - dates = models.ForeignKey(ObjectDates) - _objectname = None - - class Meta: - abstract = True - - @property - def creation(self): - return self.dates.creation - - @property - def last_modification(self): - return self.dates.last_modification - - @property - def objectname(self): - if self._objectname is None: - return self.__class__.__name__ - return self._objectname - - def post_create(self, creator): - grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("%sCreated" % self.objectname, creator, self) - - def save(self, *args, **kwargs): - ObjectDates.set_for_object(self) - if "creator" in kwargs: - creator = kwargs["creator"] - del kwargs["creator"] - else: - creator = None - super(AdminObject, self).save(*args, **kwargs) - if creator is not None: - self.post_create(creator) - - def delete(self): - events.raiseEvent("%sDeleted" % self.objectname, self) - ungrant_access_to_object(self) - super(AdminObject, self).delete() diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py deleted file mode 100644 index 8d996def4..000000000 --- a/modoboa/extensions/admin/models/domain.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Models related to domains management.""" - -from django.contrib.contenttypes import generic -from django.db import models -from django.db.models.manager import Manager -from django.utils.translation import ugettext as _, ugettext_lazy - -import reversion - -from .base import AdminObject -from modoboa.core.models import User, ObjectAccess -from modoboa.lib import events, parameters -from modoboa.lib.exceptions import BadRequest, Conflict - - -class DomainManager(Manager): - - def get_for_admin(self, admin): - """Return the domains belonging to this admin - - The result is a ``QuerySet`` object, so this function can be used - to fill ``ModelChoiceField`` objects. - """ - if admin.is_superuser: - return self.get_query_set() - return self.get_query_set().filter(owners__user=admin) - - -class Domain(AdminObject): - - """Mail domain.""" - - name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True, - help_text=ugettext_lazy("The domain name")) - quota = models.IntegerField() - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this domain"), - default=True - ) - owners = generic.GenericRelation(ObjectAccess) - - objects = DomainManager() - - class Meta: - permissions = ( - ("view_domain", "View domain"), - ("view_domains", "View domains"), - ) - ordering = ["name"] - app_label = 'admin' - - @property - def domainalias_count(self): - return self.domainalias_set.count() - - @property - def mailbox_count(self): - return self.mailbox_set.count() - - @property - def mbalias_count(self): - return self.alias_set.count() - - @property - def identities_count(self): - """Total number of identities in this domain.""" - return self.mailbox_set.count() + self.alias_set.count() - - @property - def tags(self): - return [{"name": "domain", "label": _("Domain"), "type": "dom"}] - - @property - def admins(self): - """Return the domain administrators of this domain - - :return: a list of User objects - """ - return [oa.user for oa in self.owners.filter(user__is_superuser=False)] - - @property - def aliases(self): - return self.domainalias_set - - def add_admin(self, account): - """Add a new administrator for this domain - - :param User account: the administrotor to add - """ - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(account, self) - for mb in self.mailbox_set.all(): - if mb.user.has_perm("admin.add_domain"): - continue - grant_access_to_object(account, mb) - grant_access_to_object(account, mb.user) - for al in self.alias_set.all(): - grant_access_to_object(account, al) - - def remove_admin(self, account): - """Remove an administrator of this domain. - - :param User account: administrator to remove - """ - from modoboa.lib.permissions import \ - ungrant_access_to_object, get_object_owner - - if get_object_owner(self) == account: - events.raiseEvent('DomainOwnershipRemoved', account, self) - ungrant_access_to_object(self, account) - for mb in self.mailbox_set.all(): - if mb.user.has_perm("admin.add_domain"): - continue - ungrant_access_to_object(mb, account) - ungrant_access_to_object(mb.user, account) - for al in self.alias_set.all(): - ungrant_access_to_object(al, account) - - def delete(self, fromuser, keepdir=False): - """Custom delete method. - """ - from modoboa.lib.permissions import ungrant_access_to_objects - from .mailbox import Quota - - if self.domainalias_set.count(): - events.raiseEvent("DomainAliasDeleted", self.domainalias_set.all()) - ungrant_access_to_objects(self.domainalias_set.all()) - if self.alias_set.count(): - events.raiseEvent("MailboxAliasDeleted", self.alias_set.all()) - ungrant_access_to_objects(self.alias_set.all()) - if parameters.get_admin("AUTO_ACCOUNT_REMOVAL") == "yes": - for account in User.objects.filter(mailbox__domain__name=self.name): - account.delete(fromuser, keepdir) - elif self.mailbox_set.count(): - Quota.objects.filter(username__contains='@%s' % self.name).delete() - events.raiseEvent("MailboxDeleted", self.mailbox_set.all()) - ungrant_access_to_objects(self.mailbox_set.all()) - super(Domain, self).delete() - - def __str__(self): - return self.name - - def from_csv(self, user, row): - """Create a new domain from a CSV entry. - - The expected fields order is the following:: - - "domain", name, quota, enabled - - :param ``core.User`` user: user creating the domain - :param str row: a list containing domain's definition - """ - if len(row) < 4: - raise BadRequest(_("Invalid line")) - self.name = row[1].strip() - try: - Domain.objects.get(name=self.name) - except Domain.DoesNotExist: - pass - else: - raise Conflict - try: - self.quota = int(row[2].strip()) - except ValueError: - raise BadRequest( - _("Invalid quota value for domain '%s'" % self.name)) - self.enabled = (row[3].strip() in ["True", "1", "yes", "y"]) - self.save(creator=user) - - def to_csv(self, csvwriter): - csvwriter.writerow(["domain", self.name, self.quota, self.enabled]) - for dalias in self.domainalias_set.all(): - dalias.to_csv(csvwriter) - - def post_create(self, creator): - """Post creation actions. - - :param ``User`` creator: user whos created this domain - """ - super(Domain, self).post_create(creator) - for domalias in self.domainalias_set.all(): - domalias.post_create(creator) - -reversion.register(Domain) diff --git a/modoboa/extensions/admin/models/domain_alias.py b/modoboa/extensions/admin/models/domain_alias.py deleted file mode 100644 index ba784a3cb..000000000 --- a/modoboa/extensions/admin/models/domain_alias.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Models related to domain aliases management.""" - -from django.db import models -from django.utils.translation import ugettext as _, ugettext_lazy - -import reversion - -from .base import AdminObject -from .domain import Domain -from modoboa.lib.exceptions import BadRequest, Conflict - - -class DomainAlias(AdminObject): - - """Domain aliases.""" - - name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True, - help_text=ugettext_lazy("The alias name")) - target = models.ForeignKey( - Domain, verbose_name=ugettext_lazy('target'), - help_text=ugettext_lazy("The domain this alias points to") - ) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this alias"), - default=True - ) - - class Meta: - permissions = ( - ("view_domaliases", "View domain aliases"), - ) - app_label = 'admin' - - def __unicode__(self): - return self.name - - def from_csv(self, user, row): - """Create a domain alias from a CSV row - - Expected format: ["domainalias", domain alias name, targeted domain, enabled] - - :param user: a ``User`` object - :param row: a list containing the alias definition - """ - if len(row) < 4: - raise BadRequest(_("Invalid line")) - self.name = row[1].strip() - for model in [DomainAlias, Domain]: - try: - model.objects.get(name=self.name) - except model.DoesNotExist: - pass - else: - raise Conflict - domname = row[2].strip() - try: - self.target = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise BadRequest(_("Unknown domain %s" % domname)) - self.enabled = row[3].strip() in ["True", "1", "yes", "y"] - self.save(creator=user) - - def to_csv(self, csvwriter): - """Export a domain alias using CSV format - - :param csvwriter: a ``csv.writer`` object - """ - csvwriter.writerow(["domainalias", self.name, - self.target.name, self.enabled]) - -reversion.register(DomainAlias) diff --git a/modoboa/extensions/admin/models/mailbox.py b/modoboa/extensions/admin/models/mailbox.py deleted file mode 100644 index fcc46e52c..000000000 --- a/modoboa/extensions/admin/models/mailbox.py +++ /dev/null @@ -1,334 +0,0 @@ -"""Models related to mailboxes management.""" - -import os -import pwd - -from django.contrib.contenttypes.models import ContentType -from django.db import models -from django.db.models import Q -from django.db.models.manager import Manager -from django.db.models.signals import pre_delete -from django.dispatch import receiver -from django.utils.translation import ugettext as _, ugettext_lazy - -import reversion - -from .base import AdminObject -from .domain import Domain -from modoboa.core.models import User -from modoboa.lib import parameters, events -from modoboa.lib.exceptions import BadRequest, InternalError -from modoboa.lib.sysutils import exec_cmd - - -class Quota(models.Model): - - """Keeps track of Mailbox current quota.""" - - username = models.EmailField(primary_key=True, max_length=254) - bytes = models.BigIntegerField(default=0) - messages = models.IntegerField(default=0) - - class Meta: - app_label = 'admin' - - -class MailboxManager(Manager): - - """Custom manager for Mailbox.""" - - def get_for_admin(self, admin, squery=None): - """Return the mailboxes that belong to this admin. - - The result will contain the mailboxes defined for each domain that - user can see. - - :param string squery: a search query - :return: a list of ``Mailbox`` objects - """ - qf = None - if squery is not None: - if '@' in squery: - parts = squery.split('@') - addrfilter = '@'.join(parts[:-1]) - domfilter = parts[-1] - qf = ( - Q(address__contains=addrfilter) & - Q(domain__name__contains=domfilter) - ) - else: - qf = ( - Q(address__contains=squery) | - Q(domain__name__contains=squery) - ) - ids = admin.objectaccess_set \ - .filter(content_type=ContentType.objects.get_for_model(Mailbox)) \ - .values_list('object_id', flat=True) - if qf is not None: - qf = Q(pk__in=ids) & qf - else: - qf = Q(pk__in=ids) - return self.get_query_set().select_related().filter(qf) - - -class Mailbox(AdminObject): - - """User mailbox.""" - - address = models.CharField( - ugettext_lazy('address'), max_length=252, - help_text=ugettext_lazy( - "Mailbox address (without the @domain.tld part)") - ) - quota = models.PositiveIntegerField() - use_domain_quota = models.BooleanField(default=False) - domain = models.ForeignKey(Domain) - user = models.ForeignKey(User) - - objects = MailboxManager() - - class Meta: - permissions = ( - ("view_mailboxes", "View mailboxes"), - ) - app_label = 'admin' - - def __init__(self, *args, **kwargs): - super(Mailbox, self).__init__(*args, **kwargs) - self.__mail_home = None - - def __str__(self): - return self.full_address - - def __full_address(self, localpart): - return "%s@%s" % (localpart, self.domain.name) - - @property - def full_address(self): - return self.__full_address(self.address) - - @property - def enabled(self): - return self.user.is_active - - @property - def alias_count(self): - return self.alias_set.count() - - @property - def mail_home(self): - """Retrieve the home directory of this mailbox. - - The home directory refers to the place on the file system - where the mailbox data is stored. - - We ask dovecot to give us this information because there are - several patterns to understand and we don't want to implement - them. - """ - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return None - if self.__mail_home is None: - curuser = pwd.getpwuid(os.getuid()).pw_name - mbowner = parameters.get_admin("MAILBOXES_OWNER") - options = {} - if curuser != mbowner: - options['sudo_user'] = mbowner - code, output = exec_cmd( - "doveadm user %s -f home" % self.full_address, **options - ) - if code: - raise InternalError( - _("Failed to retrieve mailbox location (%s)" % output)) - self.__mail_home = output.strip() - return self.__mail_home - - @property - def alias_addresses(self): - """Return all alias address of this mailbox. - - :rtype: list of string - """ - aliases = [] - for alias in self.alias_set.all(): - aliases += [alias.full_address] - return aliases - - @property - def quota_value(self): - """Retrieve the ``Quota`` instance associated to this mailbox.""" - if not hasattr(self, "_quota_value"): - try: - self._quota_value = Quota.objects.get( - username=self.full_address) - except Quota.DoesNotExist: - return None - return self._quota_value - - @quota_value.setter - def quota_value(self, instance): - """Set the ``Quota`` for this mailbox.""" - self._quota_value = instance - - def rename_dir(self, old_mail_home): - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return - MailboxOperation.objects.create( - mailbox=self, type='rename', argument=old_mail_home - ) - - def rename(self, address, domain): - """Rename the mailbox. - - To update the associated Quota record, we must create a new - one first, update the foreign key and then we can delete the - original record! - - :param string address: the new mailbox's address (local part) - :param Domain domain: the new mailbox's domain - - """ - old_mail_home = self.mail_home - old_qvalue = self.quota_value - self.address = address - self.domain = domain - self.quota_value = Quota.objects.create( - username=self.full_address, bytes=old_qvalue.bytes, - messages=old_qvalue.messages - ) - old_qvalue.delete() - self.rename_dir(old_mail_home) - - def delete_dir(self): - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return - MailboxOperation.objects.create(type='delete', argument=self.mail_home) - - def set_quota(self, value=None, override_rules=False): - """Set or update quota's value for this mailbox. - - A value equal to 0 means the mailbox won't have any quota. The - following cases allow people to define such behaviour: - * The domain has no quota - * :keyword:`override_rules` is True - - :param integer value: the quota's value - :param bool override_rules: allow to override defined quota rules - """ - if value is None: - if self.use_domain_quota: - self.quota = self.domain.quota - else: - self.quota = 0 - elif int(value) > self.domain.quota and not override_rules: - raise BadRequest( - _("Quota is greater than the allowed domain's limit (%dM)" - % self.domain.quota) - ) - else: - self.quota = value - if not self.quota and self.domain.quota and not override_rules: - raise BadRequest(_("A quota is required")) - - def get_quota(self): - """Get quota limit. - - :rtype: int - """ - return int(self.quota_value.bytes / 1048576) - - def get_quota_in_percent(self): - """Get current quota usage. - - :rtype: int - """ - if not self.quota: - return 0 - return int( - self.quota_value.bytes / float(self.quota * 1048576) * 100 - ) - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - super(Mailbox, self).post_create(creator) - if creator.is_superuser and not self.user.has_perm("admin.add_domain"): - # A super user is creating a new mailbox. Give - # access to that mailbox (and the associated - # account) to the appropriate domain admins, - # except if the new account has a more important - # role (SuperAdmin, Reseller) - for admin in self.domain.admins: - grant_access_to_object(admin, self) - grant_access_to_object(admin, self.user) - - def save(self, *args, **kwargs): - """Custom save. - - We just make sure a quota record is defined for this mailbox. - """ - if self.quota_value is None: - self.quota_value, created = Quota.objects.get_or_create( - username=self.full_address) - super(Mailbox, self).save(*args, **kwargs) - - def delete(self, keepdir=False): - """Custom delete method - - We try to delete the associated quota in the same time (it may - has already been removed if we're deleting a domain). - - :param bool keepdir: delete the mailbox home dir on the - filesystem or not - - """ - try: - q = Quota.objects.get(username=self.full_address) - except Quota.DoesNotExist: - pass - else: - q.delete() - if not keepdir: - self.delete_dir() - super(Mailbox, self).delete() - -reversion.register(Mailbox) - - -@receiver(pre_delete, sender=Mailbox) -def mailbox_deleted_handler(sender, **kwargs): - """``Mailbox`` pre_delete signal receiver - - In order to properly handle deletions (ie. we don't want to leave - orphan records into the db), we define this custom receiver. - - It manually removes the mailbox from the aliases it is linked to - and then remove all empty aliases. - """ - from modoboa.lib.permissions import ungrant_access_to_object - - mb = kwargs['instance'] - events.raiseEvent("MailboxDeleted", mb) - ungrant_access_to_object(mb) - for alias in mb.alias_set.all(): - alias.mboxes.remove(mb) - if alias.mboxes.count() == 0: - alias.delete() - - -class MailboxOperation(models.Model): - mailbox = models.ForeignKey(Mailbox, blank=True, null=True) - type = models.CharField( - max_length=20, choices=(('rename', 'rename'), ('delete', 'delete')) - ) - argument = models.TextField() - - class Meta: - app_label = 'admin' - - def __str__(self): - if self.type == 'rename': - return 'Rename %s -> %s' % (self.argument, self.mailbox.mail_home) - return 'Delete %s' % self.argument diff --git a/modoboa/extensions/admin/modo_extension.py b/modoboa/extensions/admin/modo_extension.py deleted file mode 100644 index 15d64de5b..000000000 --- a/modoboa/extensions/admin/modo_extension.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Registration with Modoboa takes place here.""" - -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.core.extensions import ModoExtension, exts_pool -from modoboa.extensions.admin.models import ( - Domain, DomainAlias, Mailbox, Alias -) -from modoboa.lib import parameters, events -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict - - -ADMIN_EVENTS = [ - "DomainCreated", - "DomainModified", - "DomainDeleted", - "DomainOwnershipRemoved", - "ExtraDomainEntries", - "ExtraDomainMenuEntries", - "ExtraDomainFilters", - "GetDomainActions", - "GetDomainModifyLink", - "CheckDomainName", - - "DomainAliasCreated", - "DomainAliasDeleted", - - "MailboxCreated", - "MailboxDeleted", - "MailboxModified", - - "MailboxAliasCreated", - "MailboxAliasDeleted", - - "ExtraDomainForm", - "FillDomainInstances", - - "ExtraAccountForm", - "CheckExtraAccountForm", - "FillAccountInstances", - - "ExtraDomainImportHelp", - "ImportObject" -] - -PERMISSIONS = { - "DomainAdmins": [ - ["admin", "domain", "view_domain"], - ["admin", "mailbox", "add_mailbox"], - ["admin", "mailbox", "change_mailbox"], - ["admin", "mailbox", "delete_mailbox"], - ["admin", "alias", "add_alias"], - ["admin", "alias", "change_alias"], - ["admin", "alias", "delete_alias"], - ["admin", "mailbox", "view_mailboxes"], - ["admin", "alias", "view_aliases"], - ["admin", "domainalias", "view_domaliases"], - ["admin", "domainalias", "add_domainalias"], - ["admin", "domainalias", "change_domainalias"], - ["admin", "domainalias", "delete_domainalias"] - ] -} - - -class AdminConsole(ModoExtension): - name = "admin" - label = ugettext_lazy("Administration console") - version = "1.0" - description = ugettext_lazy( - "Web based console to manage domains, accounts and aliases" - ) - always_active = True - - def load(self): - from modoboa.extensions.admin.app_settings import AdminParametersForm - parameters.register( - AdminParametersForm, ugettext_lazy("Administration")) - events.declare(ADMIN_EVENTS) - -exts_pool.register_extension(AdminConsole) - - -@events.observe("GetExtraRolePermissions") -def extra_permissions(rolename): - """Return extra permissions for :kw:`rolename`.""" - return PERMISSIONS.get(rolename, []) - - -@events.observe("ExtraUprefsRoutes") -def extra_routes(): - from django.conf.urls import url - return [ - url(r'^user/forward/', - 'modoboa.extensions.admin.views.user.forward', - name="user_forward"), - ] - - -@events.observe("AdminMenuDisplay") -def admin_menu(target, user): - if target != "top_menu": - return [] - entries = [] - if user.has_perm("admin.view_domains"): - entries += [ - {"name": "domains", - "url": reverse("admin:domain_list"), - "label": _("Domains")} - ] - if user.has_perm("core.add_user") or user.has_perm("admin.add_alias"): - entries += [ - {"name": "identities", - "url": reverse("admin:identity_list"), - "label": _("Identities")}, - ] - return entries - - -@events.observe("UserMenuDisplay") -def user_menu(target, user): - if target != "uprefs_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "forward", - "class": "ajaxnav", - "url": "forward/", - "label": ugettext_lazy("Forward")} - ] - - -@events.observe("RoleChanged") -def grant_access_to_all_objects(user, role): - from django.contrib.contenttypes.models import ContentType - from modoboa.lib.permissions import grant_access_to_objects - from modoboa.core.models import User - - if role != "SuperAdmins": - return - grant_access_to_objects( - user, User.objects.all(), - ContentType.objects.get_for_model(User) - ) - grant_access_to_objects( - user, Domain.objects.all(), - ContentType.objects.get_for_model(Domain) - ) - grant_access_to_objects( - user, DomainAlias.objects.all(), - ContentType.objects.get_for_model(DomainAlias) - ) - grant_access_to_objects( - user, Mailbox.objects.all(), - ContentType.objects.get_for_model(Mailbox) - ) - grant_access_to_objects( - user, Alias.objects.all(), - ContentType.objects.get_for_model(Alias) - ) - - -@events.observe("AccountExported") -def export_admin_domains(admin): - result = [admin.mailbox_set.all()[0].quota] \ - if admin.mailbox_set.count() else [''] - if admin.group != "DomainAdmins": - return result - return result + [dom.name for dom in Domain.objects.get_for_admin(admin)] - - -@events.observe("AccountImported") -def import_account_mailbox(user, account, row): - """Handle extra fields when an account is imported. - - Expected fields: - - email address; quota; [domain; ...] - - :param User user: user importing the account - :param User account: account being imported - :param list rom: list of fields (strings) - """ - account.email = row[0].strip() - if account.email: - account.save() - mailbox, domname = split_mailbox(account.email) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise BadRequest( - _("Account import failed (%s): domain does not exist" - % account.username) - ) - if not user.can_access(domain): - raise PermDeniedException - try: - mb = Mailbox.objects.get(address=mailbox, domain=domain) - except Mailbox.DoesNotExist: - pass - else: - raise Conflict(_("Mailbox %s already exists" % account.email)) - if len(row) == 1: - quota = None - else: - try: - quota = int(row[1].strip()) - except ValueError: - raise BadRequest( - _("Account import failed (%s): wrong quota value" - % account.username) - ) - use_domain_quota = True if not quota else False - mb = Mailbox(address=mailbox, domain=domain, - user=account, use_domain_quota=use_domain_quota) - mb.set_quota(quota, override_rules=user.has_perm("admin.change_domain")) - mb.save(creator=user) - if account.group == "DomainAdmins": - for domname in row[2:]: - try: - dom = Domain.objects.get(name=domname.strip()) - except Domain.DoesNotExist: - continue - dom.add_admin(account) - - -@events.observe("AccountAutoCreated") -def account_auto_created(user): - from modoboa.core.models import User - from modoboa.extensions.admin.lib import check_if_domain_exists - from modoboa.lib.permissions import grant_access_to_object - - if parameters.get_admin("AUTO_CREATE_DOMAIN_AND_MAILBOX") == "no": - return - localpart, domname = split_mailbox(user.username) - if user.group != 'SimpleUsers' and domname is None: - return - sadmins = User.objects.filter(is_superuser=True) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - label = check_if_domain_exists( - domname, [(DomainAlias, _('domain alias'))]) - if label is not None: - return - domain = Domain(name=domname, enabled=True, quota=0) - domain.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, domain) - try: - mb = Mailbox.objects.get(domain=domain, address=localpart) - except Mailbox.DoesNotExist: - mb = Mailbox( - address=localpart, domain=domain, user=user, use_domain_quota=True - ) - mb.set_quota(override_rules=True) - mb.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, mb) - - -@events.observe("UserLogin") -def user_logged_in(request, username, password): - from modoboa.lib.cryptutils import encrypt - - if request.user.mailbox_set.count(): - request.session["password"] = encrypt(password) - - -@events.observe("AccountDeleted") -def account_deleted(account, byuser, **kwargs): - """'AccountDeleted' listener. - - When an account is deleted, we also need to remove its mailbox (if - any). - """ - if not account.mailbox_set.count(): - return - mb = account.mailbox_set.all()[0] - if not byuser.can_access(mb): - raise PermDeniedException - keep_mb_dir = kwargs.get("keep_mb_dir", True) - mb.delete(keepdir=keep_mb_dir) diff --git a/modoboa/extensions/admin/static/admin/css/admin.css b/modoboa/extensions/admin/static/admin/css/admin.css deleted file mode 100644 index 2768bf843..000000000 --- a/modoboa/extensions/admin/static/admin/css/admin.css +++ /dev/null @@ -1,33 +0,0 @@ -#searchform { - margin-bottom: 10px; -} - -span.title { - margin-left: 40px; - margin-bottom: 10px; - float: left; - font-weight: bold; - width: 180px; -} - -table th#actions { - text-align: right; -} - -table td[name="actions"] { - text-align: right; -} - -table .progress { - margin-bottom: 0 !important; -} - -table tbody tr:hover { - background-color: #F9F9F9; -} - -.search { - width: 85%; - -webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px; - padding-left: 14px; -} diff --git a/modoboa/extensions/admin/static/admin/css/settings.css b/modoboa/extensions/admin/static/admin/css/settings.css deleted file mode 100644 index 24fc269b9..000000000 --- a/modoboa/extensions/admin/static/admin/css/settings.css +++ /dev/null @@ -1,9 +0,0 @@ - -#settings_content .form-horizontal .control-label { - margin-left: 20px; - text-align: left; -} - -#settings_content > .form-horizontal .controls { - margin-left: 240px; -} \ No newline at end of file diff --git a/modoboa/extensions/admin/static/admin/js/admin.js b/modoboa/extensions/admin/static/admin/js/admin.js deleted file mode 100644 index 85eb24af9..000000000 --- a/modoboa/extensions/admin/static/admin/js/admin.js +++ /dev/null @@ -1,556 +0,0 @@ -/** - * Creates an instance of Admin. - * - * @constructor - * @param {Object} options - instance options - * @classdesc This class contains utility methods used by the admin - * application. - */ -var Admin = function(options) { - Listing.call(this, options); -}; - -Admin.prototype = { - defaults: { - deflocation: "list/", - squery: null - }, - - initialize: function(options) { - this.options = $.extend({}, this.defaults, options); - this.options.defcallback = $.proxy(this.list_cb, this); - Listing.prototype.initialize.call(this, this.options); - this.listen(); - }, - - /** - * Callback used when the initial content of the listing is - * received. - * - * @this Admin - * @param {Object} data - response of the ajax call (JSON) - */ - list_cb: function(data) { - if (data.rows) { - $("#objects_table tbody").html(data.rows); - } else { - $("#objects_table tbody").html(""); - } - this.update_listing(data); - if (data.length === 0) { - this.get_load_page_args(); - this.end_of_list_reached(); - } - }, - - /** - * Children must override this method. - */ - listen: function() { - }, - - /** - * Load a new page using an AJAX request. - * - * @this Admin - * @param {Object} e - event object - */ - page_loader: function(e) { - var $link = get_target(e); - e.preventDefault(); - if ($link.hasClass("navigation")) { - $(".sidebar li.active").removeClass("active"); - $link.parent().addClass("active"); - } - this.navobj.baseurl($link.attr("href")).update(); - }, - - importform_cb: function() { - $(".submit").one('click', function(e) { - e.preventDefault(); - if ($("#id_sourcefile").val() === "") { - return; - } - $("#import_status").css("display", "block"); - $("#import_result").html("").removeClass("alert alert-danger"); - $("#importform").submit(); - }); - }, - - importdone: function(status, msg) { - $("#import_status").css("display", "none"); - if (status == "ok") { - $("#modalbox").modal('hide'); - this.reload_listing(msg); - } else { - $("#import_result").addClass("alert alert-danger"); - $("#import_result").html(msg); - this.importform_cb(); - } - }, - - exportform_cb: function() { - $(".submit").one('click', function(e) { - e.preventDefault(); - $("#exportform").submit(); - $("#modalbox").modal('hide'); - }); - }, - - reload_listing: function(data) { - this.navobj.update(true); - if (data) { - $("body").notify("success", data, 2000); - } - } -}; - -Admin.prototype = $.extend({}, Listing.prototype, Admin.prototype); - -/** - * Creates an instance of Domains. - * - * @constructor - * @param {Object} options - instance options - * @classdesc This class extends the Admin one by adding methods - * specific to domains management. - */ -var Domains = function(options) { - Admin.call(this, options); -}; - -Domains.prototype = { - initialize: function(options) { - Admin.prototype.initialize.call(this, options); - this.options.navigation_params.push("domfilter", "srvfilter"); - this.options.eor_message = gettext("No more domain to show"); - this.register_tag_handler("dom"); - }, - - /** - * Custom callbacks declaration. - */ - listen: function() { - Admin.prototype.listen.call(this); - $(document).on("click", "a.ajaxnav", $.proxy(this.page_loader, this)); - }, - - /** - * Initialize the domain links embedded within a page. - * - * @param {Object} data - options - */ - init_domain_links: function(data) { - var deloptions = (data.handle_mailboxes) ? - {keepdir: gettext("Do not delete domain directory")} : {}; - var warnmsg = (data.auto_account_removal && data.auto_account_removal == "yes") - ? gettext("This operation will remove ALL data associated to this domain.") - : gettext("This operation will remove all data associated to this domain, excepting accounts."); - - $("a[name=deldomain]").confirm({ - question: function() { return this.$element.attr('title'); }, - method: "POST", - warning: warnmsg, - checkboxes: deloptions, - success_cb: $.proxy(this.reload_listing, this) - }); - $(document).trigger('domain_listing_refresh'); - }, - - /** - * Navigation callback: default. - * - * @param {Object} data - response of the ajax call (JSON) - */ - list_cb: function(data) { - Admin.prototype.list_cb.call(this, data); - this.init_domain_links(data); - }, - - /** - * A new page has been received, inject it. - * - * @param {Object} data - page content - * @param {string} direction - - */ - add_new_page: function(data, direction) { - Admin.prototype.add_new_page.call(this, data, direction); - this.init_domain_links(data); - }, - - change_inputs_state: function(value) { - $("#id_dom_admin_username").attr("disabled", value); - $("input[name=create_aliases]").attr("disabled", value); - }, - - create_dom_admin_changed: function(e) { - var $target = $(e.target); - this.change_inputs_state(($target.val() == "yes") ? false : true); - }, - - /** - * Initialize the main form contained in the domain edition modal. - * - * @this Domains - */ - generalform_init: function() { - $('input:text:visible:first').focus(); - $("#original_aliases").dynamic_input({ - input_added: function($row) { - $row.find("label").html(""); - }, - input_removed: function($input) { - $input.parents(".form-group").remove(); - return true; - } - }); - }, - - optionsform_init: function() { - $("input[name=create_dom_admin]").click($.proxy(this.create_dom_admin_changed, this)); - this.change_inputs_state( - $("input[name=create_dom_admin]:checked").val() == "yes" ? false : true - ); - this.optionsform_prefill(); - }, - - optionsform_prefill: function() { - var $span = $("#id_dom_admin_username").next("span"); - $span.html("@" + $("#id_name").val()); - }, - - domadminsform_init: function() { - $("a[name=removeperm]").click(function(e) { - var $tr = $(this).parents('tr'); - simple_ajax_request.apply(this, [e, { - ok_cb: function(resp) { - $tr.remove(); - if (!$("#domadmins").find("tr").length) { - $("#admins").html('
' - + gettext("No domain administrator defined") + "
"); - } - } - }]); - }); - }, - - newdomain_cb: function() { - this.generalform_init(); - this.optionsform_init(); - $("#wizard").cwizard({ - formid: "domform", - transition_callbacks: { - 1: this.optionsform_prefill - }, - error_callbacks: { - 2: $.proxy(this.optionsform_init, this) - }, - success_callback: $.proxy(this.reload_listing, this) - }); - }, - - domainform_cb: function() { - this.generalform_init(); - this.domadminsform_init(); - $(".submit").one('click', $.proxy(function(e) { - simple_ajax_form_post(e, { - formid: "domform", - error_cb: $.proxy(this.domainform_cb, this), - reload_on_success: false, - success_cb: $.proxy(this.reload_listing, this) - }); - }, this)); - $(document).trigger('domform_init'); - } -}; - -Domains.prototype = $.extend({}, Admin.prototype, Domains.prototype); - -/** - * Creates an instance of Identities. - * - * @constructor - * @param {Object} options - instance options - * @classdesc This class extends the Admin one by adding methods - * specific to identities management. - */ -var Identities = function(options) { - Admin.call(this, options); -}; - -Identities.prototype = { - initialize: function(options) { - Admin.prototype.initialize.call(this, options); - this.options.navigation_params.push("idtfilter", "grpfilter"); - this.options.eor_message = gettext("No more identity to show"); - this.domain_list = []; - this.register_tag_handler("idt"); - this.register_tag_handler("grp", this.grp_tag_handler); - }, - - listen: function() { - Admin.prototype.listen.call(this); - $(document).on("click", "a.ajaxnav", $.proxy(this.page_loader, this)); - $(document).on("click", "a.ajaxcall", $.proxy(this.send_call, this)); - }, - - /** - * Send an ajax call. - */ - send_call: function(evt) { - var $link = get_target(evt, "a"); - var $this = this; - var method = $link.attr("data-call_method"); - - if (method === undefined) { - method = "GET"; - } - evt.preventDefault(); - $.ajax({ - url: $link.attr("href"), - type: method, - dataType: "json" - }).done(function(data) { - $this.reload_listing(data.respmsg); - }); - }, - - /** - * Initialize the links embedded within a page. - * - * @param {Object} data - options - */ - init_identity_links: function(data) { - var deloptions = {}; - - if (data.handle_mailboxes == "yes") { - deloptions = {keepdir: gettext("Do not delete mailbox directory")}; - } - - $("a[name=delaccount]").confirm({ - question: function() { return this.$element.attr('title'); }, - method: "POST", - checkboxes: deloptions, - success_cb: $.proxy(this.reload_listing, this) - }); - $("a[name=delalias]").confirm({ - question: function() { return this.$element.attr('title'); }, - method: "DELETE", - success_cb: $.proxy(this.reload_listing, this) - }); - }, - - /** - * Add a type to let the server know what kind of object we expect. - * - * @this Listing - */ - get_load_page_args: function() { - var args = Admin.prototype.get_load_page_args.call(this); - - if (this.navobj.getbaseurl() == "list") { - args.objtype = "identity"; - this.options.eor_message = gettext("No more identity to show"); - } else { - args.objtype = "quota"; - this.options.eor_message = gettext("No more quota to show"); - } - return args; - }, - - /** - * A new page has been received, inject it. - * - * @param {Object} data - page content - * @param {string} direction - - */ - add_new_page: function(data, direction) { - Admin.prototype.add_new_page.call(this, data, direction); - this.init_identity_links(data); - }, - - /** - * Navigation callback: default. - * - * @param {Object} data - response of the ajax call (JSON) - */ - list_cb: function(data) { - if ($(".sidebar li.active").length === 0) { - var menu = this.navobj.getbaseurl(); - if (menu == "list") { - menu = "identities"; - } - $("a[name={0}]".format(menu)).parent().addClass("active"); - } - $("#objects_table thead tr").html(data.headers); - Admin.prototype.list_cb.call(this, data); - this.init_identity_links(data); - }, - - grp_tag_handler: function(tag, $link) { - if (this.navobj.getparam(tag + "filter") === undefined && - $link.hasClass(tag)) { - var text = $link.attr("name"); - this.navobj - .setparam("idtfilter", "account") - .setparam(tag + "filter", text) - .update(); - if ($("a[name=idt]").length === 0) { - $("#taglist").append(this.make_tag("account", "idt")); - } - $("#taglist").append(this.make_tag(text, tag)); - return true; - } - return false; - }, - - /** - * Retrieve a list of domain from the server. - * - * @this Identities - * @return {Array} a list of domain names - */ - get_domain_list: function() { - if (!this.domain_list.length) { - $.ajax({ - url: this.options.domain_list_url, - dataType: "json", - async: false - }).done($.proxy(function(data) { - this.domain_list = data; - }, this)); - } - return this.domain_list; - }, - - simpleuser_mode: function() { - $("#id_username").autocompleter({ - from_character: "@", - choices: $.proxy(this.get_domain_list, this) - }); - $("#id_email").addClass("disabled") - .attr("readonly", "") - .autocompleter("unbind"); - }, - - normal_mode: function() { - $("#id_email").removeClass("disabled") - .attr("readonly", null) - .autocompleter("listen"); - }, - - generalform_init: function(notrigger) { - $("#id_role").change($.proxy(function(e) { - var $this = $(e.target); - var value = $this.val(); - - if (value == "SimpleUsers" || value === "") { - this.simpleuser_mode(); - } else { - this.normal_mode(); - } - }, this)); - if (notrigger !== undefined && notrigger) { - return; - } - $("#id_role").trigger("change"); - }, - - mailform_init: function() { - $("#id_aliases").autocompleter({ - from_character: "@", - choices: $.proxy(this.get_domain_list, this) - }); - $("#original_aliases").dynamic_input({ - input_added: function($row) { - $row.find("label").html(""); - }, - input_removed: function($input) { - $input.parents(".form-group").remove(); - return true; - } - }); - $("#id_email").autocompleter({ - from_character: "@", - choices: $.proxy(this.get_domain_list, this) - }); - if ($("#id_role").length) { - $("#id_role").trigger("change"); - } else { - this.simpleuser_mode(); - } - $("#id_domains").autocompleter({ - choices: $.proxy(this.get_domain_list, this) - }); - $("#original_domains").dynamic_input({ - input_added: function($row) { - $row.find("label").html(""); - }, - input_removed: function($input) { - $input.parents(".form-group").remove(); - return true; - } - }); - activate_widget.call($("#id_quota_act")); - }, - - accountform_init: function() { - this.generalform_init(true); - this.mailform_init(); - }, - - mailform_prefill: function() { - var $role = $("#id_role"); - if (!$role.length || $role.val() === "" || $role.val() == "SimpleUsers") { - $("#id_email").val($("#id_username").val()); - } - }, - - newaccount_cb: function() { - this.accountform_init(); - $("#wizard").cwizard({ - formid: "newaccount_form", - transition_callbacks: { - 1: this.mailform_prefill - }, - success_callback: $.proxy(this.reload_listing, this) - }); - }, - - editaccount_cb: function() { - this.accountform_init(); - $('.submit').on('click', $.proxy(function(e) { - simple_ajax_form_post(e, { - formid: "accountform", - reload_on_success: false, - success_cb: $.proxy(this.reload_listing, this) - }); - }, this)); - }, - - aliasform_cb: function() { - $("#id_email").autocompleter({ - from_character: "@", - choices: $.proxy(this.get_domain_list, this) - }); - $("#original_recipients").dynamic_input({ - input_added: function($row) { - $row.find("label").html(""); - }, - input_removed: function($input) { - $input.parents(".form-group").remove(); - return true; - } - }); - $(".submit").on('click', $.proxy(function(e) { - simple_ajax_form_post(e, { - formid: "aliasform", - reload_on_success: false, - success_cb: $.proxy(this.reload_listing, this) - }); - }, this)); - } - -}; - -Identities.prototype = $.extend({}, Admin.prototype, Identities.prototype); diff --git a/modoboa/extensions/admin/tables.py b/modoboa/extensions/admin/tables.py deleted file mode 100644 index d8f4f1c8d..000000000 --- a/modoboa/extensions/admin/tables.py +++ /dev/null @@ -1,15 +0,0 @@ -# coding: utf-8 -from django.utils.translation import ugettext_lazy -from modoboa.lib import tables - - -class ExtensionsTable(tables.Table): - idkey = "id" - selection = tables.SelectionColumn("selection", width="4%", header=False) - label = tables.Column("label", label=ugettext_lazy("Name"), width="15%") - version = tables.Column( - "version", label=ugettext_lazy("Version"), width="6%" - ) - descr = tables.Column("description", label=ugettext_lazy("Description")) - - cols_order = ["selection", "label", "version", "descr"] diff --git a/modoboa/extensions/admin/templates/admin/account_general_form.html b/modoboa/extensions/admin/templates/admin/account_general_form.html deleted file mode 100644 index 3df108317..000000000 --- a/modoboa/extensions/admin/templates/admin/account_general_form.html +++ /dev/null @@ -1,12 +0,0 @@ -{% load form_tags %} -{% render_field form.role %} -{% render_field form.username %} -{% render_field form.first_name %} -{% render_field form.last_name %} -{% render_field form.password1 %} -{% render_field form.password2 %} -{% if form.domain_is_disabled %} -{% include "common/generic_field.html" with field=form.is_active label_width="col-sm-4" inline_message=_("Domain is disabled") %} -{% else %} -{% render_field form.is_active %} -{% endif %} diff --git a/modoboa/extensions/admin/templates/admin/aliasform.html b/modoboa/extensions/admin/templates/admin/aliasform.html deleted file mode 100644 index 5eef9092c..000000000 --- a/modoboa/extensions/admin/templates/admin/aliasform.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "common/generic_modal_form.html" %}{% load form_tags %} -{% block formbody %} -{% render_field form.email %} -{% render_fields_group form "recipients" %} -{% if not form.instance.pk or form.instance.domain.enabled %} -{% render_field form.enabled %} -{% else %} -{% include "common/generic_field.html" with field=form.enabled label_width="col-sm-4" inline_message=_("Domain is disabled") extra_classes="info" %} -{% endif %} -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/domain_general_form.html b/modoboa/extensions/admin/templates/admin/domain_general_form.html deleted file mode 100644 index 7a79f0b51..000000000 --- a/modoboa/extensions/admin/templates/admin/domain_general_form.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load i18n form_tags %} -{% render_field form.name %} -{% render_field form.quota appended_text=_("MB") %} -{% render_fields_group form "aliases" %} -{% render_field form.enabled %} diff --git a/modoboa/extensions/admin/templates/admin/domain_options_form.html b/modoboa/extensions/admin/templates/admin/domain_options_form.html deleted file mode 100644 index 0993c11fa..000000000 --- a/modoboa/extensions/admin/templates/admin/domain_options_form.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load i18n form_tags %} -{% if form.fields %} -{% render_field form.create_dom_admin %} -{% render_field form.dom_admin_username appended_text="@" %} -{% render_field form.create_aliases %} -{% else %} -
{% trans "You can't create a new domain administrator" %}
-{% endif %} diff --git a/modoboa/extensions/admin/templates/admin/domain_statistics.html b/modoboa/extensions/admin/templates/admin/domain_statistics.html deleted file mode 100644 index 2be458dab..000000000 --- a/modoboa/extensions/admin/templates/admin/domain_statistics.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "admin/header.html" %} - -{% load i18n %} -{% load admin_tags %} - -{% block leftcol %} - - -{% get_extra_admin_content user "leftcol" selection %} -{% endblock %} - -{% block apparea %} -

{% trans "Statistics" %} {% trans "Domains and identities" %}

-
- -
- {% if user.group == "SuperAdmins" %} -
-
-

{% trans "Global counters" %}

-
- - - - - - - - - - - - - - -
{% trans "Type" %}{% trans "Quantity" %}
{% trans "Domains" %}{{ domains_counter }}
{% trans "Domain aliases" %}{{ domain_aliases_counter }}
{% trans "Identities" %}{{ identities_counter }}
-
-
- {% endif %} - -
-
-

{% trans "Per-domain counters" %}

-
- - - - - - {% for domain in domains %} - - - - {% endfor %} -
{% trans "Domain" %}{% trans "Identities" %}
{{ domain.name }}{{ domain.identities_count }}
-
-
- -
- -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/domains.html b/modoboa/extensions/admin/templates/admin/domains.html deleted file mode 100644 index ec4025f0c..000000000 --- a/modoboa/extensions/admin/templates/admin/domains.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "admin/listing.html" %}{% load i18n admin_tags lib_tags %} - -{% block pagetitle %}{% trans "Domains" %}{% endblock %} - -{% block extra_css %} -{{ block.super }} -{% extra_static_content "domains" "css" user %} -{% endblock %} - -{% block extra_js %} -{{ block.super }} - -{% extra_static_content "domains" "js" user %} -{% endblock %} - -{% block actions %}{% domains_menu "domains" user %}{% endblock %} - -{% block table_header %} -{% trans "Name" %} -{% trans "Alias(es)" %} -{% trans "Tags" %} -{% trans "Actions" %} -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/domains_table.html b/modoboa/extensions/admin/templates/admin/domains_table.html deleted file mode 100644 index c79777a8b..000000000 --- a/modoboa/extensions/admin/templates/admin/domains_table.html +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n admin_tags lib_tags %} -{% for domain in domains %} - - {% domain_modify_link domain %}{% if not domain.enabled %} ({% trans "disabled" %}){% endif %} - {% domain_aliases domain %} - {% render_tags domain.tags %} - {% domain_actions user domain %} - -{% endfor %} diff --git a/modoboa/extensions/admin/templates/admin/editdomainform.html b/modoboa/extensions/admin/templates/admin/editdomainform.html deleted file mode 100644 index 09d19065f..000000000 --- a/modoboa/extensions/admin/templates/admin/editdomainform.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "common/tabforms.html" %}{% load i18n lib_tags admin_tags %} -{% block extratabs %} -
  • - {% trans "Administrators" %} -
  • -
  • - {% trans "Miscellaneous" %} -
  • -{% endblock %} -{% block extratabs_content %} -
    {% if domadmins %} - - - {% for da in domadmins %} - - - - {% endfor %} -
    {% trans "Username" %}{% trans "Name" %}
    {{ da.username }}{{ da.fullname }}{% if perms.admin.change_domain %}{% domadmin_actions da.id domain.id %}{% endif %}
    {% else %}
    {% trans "No domain administrator defined" %}
    {% endif %} -
    -
    -
    {% trans "Creation date" %}{{ domain.creation }}
    -
    {% trans "Last modification date" %}{{ domain.last_modification }}
    -
    {% trans "Domain aliases" %}{{ domain.domainalias_count }}
    -
    {% trans "Mailboxes" %}{{ domain.mailbox_count }}
    -
    {% trans "Mailbox aliases" %}{{ domain.mbalias_count }}
    -
    -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/forward.html b/modoboa/extensions/admin/templates/admin/forward.html deleted file mode 100644 index 3146beea6..000000000 --- a/modoboa/extensions/admin/templates/admin/forward.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load i18n form_tags %}{% load url from future %} -

    {% trans "Forward" %} {% trans "Automatically forward emails to another address" %}

    -
    -
    - {% csrf_token %} - {% render_form form %} -
    - -
    -
    diff --git a/modoboa/extensions/admin/templates/admin/header.html b/modoboa/extensions/admin/templates/admin/header.html deleted file mode 100644 index 1dacea7a8..000000000 --- a/modoboa/extensions/admin/templates/admin/header.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "twocols.html" %} -{% load static %} - -{% block extra_css %} - -{% endblock %} - -{% block extra_js %} - - - -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/identities.html b/modoboa/extensions/admin/templates/admin/identities.html deleted file mode 100644 index dde5b0e5f..000000000 --- a/modoboa/extensions/admin/templates/admin/identities.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "admin/listing.html" %} -{% load admin_tags lib_tags i18n %}{% load url from future %} -{% load static %} - -{% block pagetitle %}{% trans "Identities" %}{% endblock %} - -{% block extra_css %} -{{ block.super }} -{% extra_static_content "identities" "css" user %} -{% endblock %} - -{% block extra_js %} -{{ block.super }} - - -{% extra_static_content "identities" "js" user %} -{% endblock %} - -{% block actions %}{% identities_menu user "identities" %}{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/identities_table.html b/modoboa/extensions/admin/templates/admin/identities_table.html deleted file mode 100644 index 15fad1e00..000000000 --- a/modoboa/extensions/admin/templates/admin/identities_table.html +++ /dev/null @@ -1,10 +0,0 @@ -{% load i18n admin_tags lib_tags %} -{% for ident in identities %} - {% check_identity_status ident as identity_is_enabled %} - - {% identity_modify_link ident %}{% if not identity_is_enabled %} (disabled){% endif %} - {{ ident.name_or_rcpt }} - {% render_tags ident.tags %} - {% identity_actions user ident %} - -{% endfor %} diff --git a/modoboa/extensions/admin/templates/admin/identity_headers.html b/modoboa/extensions/admin/templates/admin/identity_headers.html deleted file mode 100644 index 53c18e745..000000000 --- a/modoboa/extensions/admin/templates/admin/identity_headers.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load i18n %} -{% trans "Name" %} -{% trans "Fullname/Recipient" %} -{% trans "Tags" %} -{% trans "Actions" %} diff --git a/modoboa/extensions/admin/templates/admin/import_done.html b/modoboa/extensions/admin/templates/admin/import_done.html deleted file mode 100644 index 55a3191b0..000000000 --- a/modoboa/extensions/admin/templates/admin/import_done.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/modoboa/extensions/admin/templates/admin/importform.html b/modoboa/extensions/admin/templates/admin/importform.html deleted file mode 100644 index 8e15acf69..000000000 --- a/modoboa/extensions/admin/templates/admin/importform.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "common/generic_modal_form.html" %} -{% load form_tags i18n %} -{% load static %} -{% block formbody %} -
    -

    {% trans "Help" %}

    - {{ helptext|safe }} -
    -{% render_form form %} - - -
    -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/listing.html b/modoboa/extensions/admin/templates/admin/listing.html deleted file mode 100644 index a5c5c7dbb..000000000 --- a/modoboa/extensions/admin/templates/admin/listing.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "admin/header.html" %}{% load i18n admin_tags %} -{% load static %} - -{% block extra_css %} -{{ block.super }} - -{% endblock %} - -{% block extra_js %} -{{ block.super }} - - - - - -{% endblock %} - -{% block leftcol %} - - -{% get_extra_admin_content user "leftcol" selection %} -{% endblock %} - -{% block apparea %} -
    -
    - {% include "admin/searchform.html" %} -
    -
    -
    - -
    - - - - {% block table_header %}{% endblock %} - - - - -
    -
    -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/mailboxes_raw.html b/modoboa/extensions/admin/templates/admin/mailboxes_raw.html deleted file mode 100644 index 9723f4a20..000000000 --- a/modoboa/extensions/admin/templates/admin/mailboxes_raw.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load i18n %} -{% if target == "permissions" %} - -{% endif %} -{% for mb in mailboxes %} -{% if target == "permissions" %} - -{% else %} - -{% endif %} -{% endfor %} diff --git a/modoboa/extensions/admin/templates/admin/mailform.html b/modoboa/extensions/admin/templates/admin/mailform.html deleted file mode 100644 index 4b7b6ec63..000000000 --- a/modoboa/extensions/admin/templates/admin/mailform.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load form_tags %} -{% render_field form.email %} -{% include "common/generic_field.html" with field=form.quota help_display_mode="tooltip" label_width="col-sm-4" appended_text=_("MB") activator=True activator_value=form.quota_act.value deactivate_if_empty=False activator_label=_("Use domain default value") activator_size="col-sm-5" %} -{% render_extra_fields form %} -{% render_fields_group form "aliases" %} diff --git a/modoboa/extensions/admin/templates/admin/newdomainform.html b/modoboa/extensions/admin/templates/admin/newdomainform.html deleted file mode 100644 index 8b4d2ba29..000000000 --- a/modoboa/extensions/admin/templates/admin/newdomainform.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "common/generic_modal_form.html" %}{% load form_tags %}{% block formbody %} -{% render_field form.name %} -{% render_field form.quota appended_text=_("MB") %} -{% render_fields_group form "aliases" %} -{% render_field form.enabled %} -{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/permsform.html b/modoboa/extensions/admin/templates/admin/permsform.html deleted file mode 100644 index b1856a04e..000000000 --- a/modoboa/extensions/admin/templates/admin/permsform.html +++ /dev/null @@ -1,2 +0,0 @@ -{% load form_tags %} -{% render_fields_group form "domains" %} diff --git a/modoboa/extensions/admin/templates/admin/quota_headers.html b/modoboa/extensions/admin/templates/admin/quota_headers.html deleted file mode 100644 index e496973e0..000000000 --- a/modoboa/extensions/admin/templates/admin/quota_headers.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load i18n %} -{% trans "Address" %} -{% trans "Value" %} -{% trans "Limit" %} -{% trans "Usage (%)" %} diff --git a/modoboa/extensions/admin/templates/admin/quotas.html b/modoboa/extensions/admin/templates/admin/quotas.html deleted file mode 100644 index 266474b21..000000000 --- a/modoboa/extensions/admin/templates/admin/quotas.html +++ /dev/null @@ -1,15 +0,0 @@ -{% load lib_tags admin_tags %} - -{% for mbox in mboxes %} - - {% identity_modify_link mbox.user "mail" %} - {{ mbox.get_quota }}M - {{ mbox.quota }}M - -
    -
    -
    - - -{% endfor %} diff --git a/modoboa/extensions/admin/templates/admin/searchform.html b/modoboa/extensions/admin/templates/admin/searchform.html deleted file mode 100644 index bac694e17..000000000 --- a/modoboa/extensions/admin/templates/admin/searchform.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load i18n %} -
    -
    -
    - - -
    -
    -
    diff --git a/modoboa/extensions/admin/templatetags/__init__.py b/modoboa/extensions/admin/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/admin/templatetags/admin_tags.py b/modoboa/extensions/admin/templatetags/admin_tags.py deleted file mode 100644 index d05b8ad9c..000000000 --- a/modoboa/extensions/admin/templatetags/admin_tags.py +++ /dev/null @@ -1,280 +0,0 @@ -""" -Admin extension tags. -""" - -from django import template -from django.core.urlresolvers import reverse -from django.template.loader import render_to_string -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.lib import events -from modoboa.lib.templatetags.lib_tags import render_link -from modoboa.lib.web_utils import render_actions - -register = template.Library() - -genders = { - "Enabled": (ugettext_lazy("enabled_m"), ugettext_lazy("enabled_f")) -} - - -@register.simple_tag -def domains_menu(selection, user): - """Specific menu for domain related operations. - - Corresponds to the menu visible on the left column when you go to - *Domains*. - - :param str selection: menu entry currently selected - :param ``User`` user: connected user - :rtype: str - :return: rendered menu (as HTML) - """ - domain_list_url = ( - "list/" if selection != "statistics" else reverse("admin:domain_list") - ) - entries = [ - {"name": "domains", - "label": _("List domains"), - "img": "fa fa-user", - "class": "ajaxnav navigation", - "url": domain_list_url}, - {"name": "statistics", - "label": _("Statistics"), - "img": "fa fa-line-chart", - "class": "navigation", - "url": reverse("admin:domain_statistics")} - ] - if user.has_perm("admin.add_domain"): - entries += [ - {"name": "newdomain", - "label": _("Add domain"), - "img": "fa fa-plus", - "modal": True, - "modalcb": "admin.newdomain_cb", - "url": reverse("admin:domain_add")}, - ] - entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user) - entries += [ - {"name": "import", - "label": _("Import"), - "img": "fa fa-folder-open", - "url": reverse("admin:domain_import"), - "modal": True, - "modalcb": "admin.importform_cb"}, - {"name": "export", - "label": _("Export"), - "img": "fa fa-share-alt", - "url": reverse("admin:domain_export"), - "modal": True, - "modalcb": "admin.exportform_cb"} - ] - - return render_to_string('common/menulist.html', { - "entries": entries, - "selection": selection, - "user": user - }) - - -@register.simple_tag -def identities_menu(user, selection=None): - """Menu specific to the Identities page. - - :param ``User`` user: the connecter user - :rtype: str - :return: the rendered menu - """ - entries = [ - {"name": "identities", - "label": _("List identities"), - "img": "fa fa-user", - "class": "ajaxnav navigation", - "url": "list/"}, - {"name": "quotas", - "label": _("List quotas"), - "img": "fa fa-hdd-o", - "class": "ajaxnav navigation", - "url": "quotas/"}, - {"name": "newaccount", - "label": _("Add account"), - "img": "fa fa-plus", - "modal": True, - "modalcb": "admin.newaccount_cb", - "url": reverse("admin:account_add")}, - {"name": "newalias", - "label": _("Add alias"), - "img": "fa fa-plus", - "modal": True, - "modalcb": "admin.aliasform_cb", - "url": reverse("admin:alias_add")}, - {"name": "newforward", - "label": _("Add forward"), - "img": "fa fa-plus", - "modal": True, - "modalcb": "admin.aliasform_cb", - "url": reverse("admin:forward_add")}, - {"name": "newdlist", - "label": _("Add distribution list"), - "img": "fa fa-plus", - "modal": True, - "modalcb": "admin.aliasform_cb", - "url": reverse("admin:dlist_add")}, - {"name": "import", - "label": _("Import"), - "img": "fa fa-folder-open", - "url": reverse("admin:identity_import"), - "modal": True, - "modalcb": "admin.importform_cb"}, - {"name": "export", - "label": _("Export"), - "img": "fa fa-share-alt", - "url": reverse("admin:identity_export"), - "modal": True, - "modalcb": "admin.exportform_cb" - } - ] - - return render_to_string('common/menulist.html', { - "entries": entries, - "user": user - }) - - -@register.simple_tag -def domain_actions(user, domain): - actions = [] - if domain.__class__.__name__ == 'Domain': - actions = [ - {"name": "listidentities", - "url": "{0}#list/?searchquery=@{1}".format( - reverse("admin:identity_list"), domain.name), - "title": _("View the domain's identities"), - "img": "fa fa-user"} - ] - if user.has_perm("admin.delete_domain"): - actions.append({ - "name": "deldomain", - "url": reverse("admin:domain_delete", args=[domain.id]), - "title": _("Delete %s?" % domain.name), - "img": "fa fa-trash" - }) - else: - actions = events.raiseQueryEvent('GetDomainActions', user, domain) - - return render_actions(actions) - - -@register.simple_tag -def identity_actions(user, ident): - name = ident.__class__.__name__ - objid = ident.id - if name == "User": - actions = events.raiseQueryEvent("ExtraAccountActions", ident) - actions += [ - {"name": "delaccount", - "url": reverse("admin:account_delete", args=[objid]), - "img": "fa fa-trash", - "title": _("Delete %s?" % ident.username)}, - ] - else: - actions = [ - {"name": "delalias", - "url": reverse("admin:alias_delete") + "?selection=%s" % objid, - "img": "fa fa-trash", - "title": _("Delete %s?" % ident.full_address)}, - ] - return render_actions(actions) - - -@register.assignment_tag -def check_identity_status(identity): - """Check if identity is enabled or not.""" - if identity.__class__.__name__ == "User": - if identity.mailbox_set.count() \ - and not identity.mailbox_set.all()[0].domain.enabled: - return False - elif not identity.is_active: - return False - elif not identity.enabled or not identity.domain.enabled: - return False - return True - - -@register.simple_tag -def domain_modify_link(domain): - linkdef = {"label": domain.name, "modal": True} - if domain.__class__.__name__ == "Domain": - linkdef["url"] = reverse("admin:domain_change", args=[domain.id]) - linkdef["modalcb"] = "admin.domainform_cb" - else: - tmp = events.raiseDictEvent('GetDomainModifyLink', domain) - for key in ['url', 'modalcb']: - linkdef[key] = tmp[key] - return render_link(linkdef) - - -@register.simple_tag -def domain_aliases(domain): - """Display domain aliases of this domain. - - :param domain: - :rtype: str - """ - if not domain.aliases.count(): - return '---' - res = '' - for alias in domain.aliases.all(): - res += '%s
    ' % alias.name - return res - - -@register.simple_tag -def identity_modify_link(identity, active_tab='default'): - """Return the appropriate modification link. - - According to the identity type, a specific modification link (URL) - must be used. - - :param identity: a ``User`` or ``Alias`` instance - :param str active_tab: the tab to display - :rtype: str - """ - linkdef = {"label": identity.identity, "modal": True} - if identity.__class__.__name__ == "User": - linkdef["url"] = reverse("admin:account_change", args=[identity.id]) - linkdef["url"] += "?active_tab=%s" % active_tab - linkdef["modalcb"] = "admin.editaccount_cb" - else: - linkdef["url"] = reverse("admin:alias_change", args=[identity.id]) - linkdef["modalcb"] = "admin.aliasform_cb" - return render_link(linkdef) - - -@register.simple_tag -def domadmin_actions(daid, domid): - actions = [{ - "name": "removeperm", - "url": "{0}?domid={1}&daid={2}".format( - reverse("admin:permission_remove"), domid, daid), - "img": "fa fa-trash", - "title": _("Remove this permission") - }] - return render_actions(actions) - - -@register.filter -def gender(value, target): - if value in genders: - trans = target == "m" and genders[value][0] or genders[value][1] - if trans.find("_") == -1: - return trans - return value - - -@register.simple_tag -def get_extra_admin_content(user, target, currentpage): - res = events.raiseQueryEvent( - "ExtraAdminContent", user, target, currentpage - ) - return "".join(res) diff --git a/modoboa/extensions/admin/tests/__init__.py b/modoboa/extensions/admin/tests/__init__.py deleted file mode 100644 index 8fc7ac21f..000000000 --- a/modoboa/extensions/admin/tests/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from .domain import DomainTestCase -from .domain_alias import DomainAliasTestCase -from .account import AccountTestCase, PermissionsTestCase -from .alias import AliasTestCase -from .import_ import ImportTestCase -from .export import ExportTestCase -from .password_schemes import PasswordSchemesTestCase -from .user import ForwardTestCase - -__all__ = [ - 'DomainTestCase', 'DomainAliasTestCase', 'AccountTestCase', - 'PermissionsTestCase', 'AliasTestCase', 'ImportTestCase', - 'ExportTestCase', 'PasswordSchemesTestCase', 'ForwardTestCase' -] diff --git a/modoboa/extensions/admin/tests/account.py b/modoboa/extensions/admin/tests/account.py deleted file mode 100644 index 6881056a4..000000000 --- a/modoboa/extensions/admin/tests/account.py +++ /dev/null @@ -1,197 +0,0 @@ -from django.core.urlresolvers import reverse - -from modoboa.core.models import User -from modoboa.extensions.admin import factories -from modoboa.extensions.admin.models import ( - Domain, Mailbox -) -from modoboa.lib.tests import ModoTestCase - - -class AccountTestCase(ModoTestCase): - fixtures = ["initial_users.json"] - - def setUp(self): - super(AccountTestCase, self).setUp() - factories.populate_database() - - def test_crud(self): - values = dict( - username="tester@test.com", first_name="Tester", last_name="Toto", - password1="toto", password2="toto", role="SimpleUsers", - quota_act=True, - is_active=True, email="tester@test.com", stepid='step2' - ) - self.ajax_post(reverse("admin:account_add"), values) - - account = User.objects.get(username="tester@test.com") - mb = account.mailbox_set.all()[0] - self.assertEqual(mb.full_address, "tester@test.com") - self.assertEqual(mb.quota, 10) - self.assertEqual(mb.enabled, True) - self.assertEqual(mb.quota_value.username, "tester@test.com") - self.assertEqual(account.username, mb.full_address) - self.assertTrue(account.check_password("toto")) - self.assertEqual(account.first_name, "Tester") - self.assertEqual(account.last_name, "Toto") - self.assertEqual(mb.domain.mailbox_count, 3) - - values["username"] = "pouet@test.com" - self.ajax_post( - reverse("admin:account_change", args=[account.id]), values - ) - mb = Mailbox.objects.get(pk=mb.id) - self.assertEqual(mb.full_address, "pouet@test.com") - self.assertEqual(mb.quota_value.username, "pouet@test.com") - self.ajax_post( - reverse("admin:account_delete", args=[account.id]), {} - ) - - def _set_quota(self, email, value, expected_status=200): - account = User.objects.get(username=email) - values = { - "username": email, "role": "SimpleUsers", "quota_act": False, - "is_active": True, "quota": value, "email": email - } - self.ajax_post( - reverse("admin:account_change", args=[account.id]), - values, status=expected_status - ) - - def test_set_nul_quota_as_superadmin(self): - self._set_quota("user@test.com", 0) - - def test_set_nul_quota_as_domainadmin(self): - self.clt.logout() - self.assertTrue( - self.clt.login(username="admin@test.com", password="toto") - ) - self._set_quota("user@test.com", 0, 400) - self.clt.logout() - self.assertTrue( - self.clt.login(username="admin@test2.com", password="toto") - ) - self._set_quota("user@test2.com", 0) - - -class PermissionsTestCase(ModoTestCase): - fixtures = ["initial_users.json"] - - def setUp(self): - super(PermissionsTestCase, self).setUp() - factories.populate_database() - self.user = User.objects.get(username='user@test.com') - self.values = dict( - username=self.user.username, role="DomainAdmins", - is_active=self.user.is_active, email="user@test.com", - quota_act=True - ) - - def tearDown(self): - self.clt.logout() - - def test_domain_admins(self): - self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values - ) - self.assertEqual(self.user.group, "DomainAdmins") - - self.values["role"] = "SimpleUsers" - self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values - ) - self.assertNotEqual(self.user.group, 'DomainAdmins') - - def test_superusers(self): - self.values["role"] = "SuperAdmins" - self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values - ) - self.assertEqual( - User.objects.get(username="user@test.com").is_superuser, True - ) - - self.values["role"] = "SimpleUsers" - self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values - ) - self.assertEqual(User.objects.get( - username="user@test.com").is_superuser, False) - - def test_self_modif(self): - self.clt.logout() - self.assertTrue( - self.clt.login(username="admin@test.com", password="toto") - ) - admin = User.objects.get(username="admin@test.com") - values = dict( - username="admin@test.com", first_name="Admin", - password1="", password2="", - quota=10, is_active=True, email="admin@test.com" - ) - self.ajax_post( - reverse("admin:account_change", args=[admin.id]), - values - ) - self.assertEqual(admin.group, "DomainAdmins") - self.assertTrue(admin.can_access(Domain.objects.get(name="test.com"))) - - values["role"] = "SuperAdmins" - self.ajax_post( - reverse("admin:account_change", args=[admin.id]), - values - ) - admin = User.objects.get(username="admin@test.com") - self.assertEqual(admin.group, "DomainAdmins") - - def test_domadmin_access(self): - self.clt.logout() - self.assertEqual( - self.clt.login(username="admin@test.com", password="toto"), - True) - response = self.clt.get(reverse("admin:domain_list")) - self.assertEqual(response.status_code, 200) - - response = self.clt.get( - reverse("admin:account_change", args=[self.user.id]), - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertNotEqual(response["Content-Type"], "application/json") - - def test_domainadmin_deletes_superadmin(self): - """Check domain admins restrictions about super admins - - When a super admin owns a mailbox and a domain admin exists - for the associated domain, this domain admin must not be able - to access the super admin. - """ - values = dict( - username="superadmin2@test.com", first_name="Super", - last_name="Admin", password1="toto", password2="toto", - role="SuperAdmins", is_active=True, - email="superadmin2@test.com", stepid='step2' - ) - self.ajax_post( - reverse("admin:account_add"), - values - ) - account = User.objects.get(username="superadmin2@test.com") - self.clt.logout() - self.clt.login(username="admin@test.com", password="toto") - self.ajax_post( - reverse("admin:account_delete", args=[account.id]), {}, 403 - ) - - def test_domainadmin_dlist_local_domain_not_owned(self): - """Check if a domain admin can use a local mailbox he can't - access as a recipient in a distribution list""" - values = dict( - email="all@test.com", - recipients="user@test.com", - recipients_1="user@test2.com", - enabled=True - ) - self.ajax_post(reverse("admin:dlist_add"), values) diff --git a/modoboa/extensions/admin/tests/alias.py b/modoboa/extensions/admin/tests/alias.py deleted file mode 100644 index 10bc60a99..000000000 --- a/modoboa/extensions/admin/tests/alias.py +++ /dev/null @@ -1,134 +0,0 @@ -from django.core.urlresolvers import reverse - -from modoboa.core.models import User -from modoboa.extensions.admin import factories -from modoboa.extensions.admin.models import ( - Alias -) -from modoboa.lib.tests import ModoTestCase - - -class AliasTestCase(ModoTestCase): - fixtures = ["initial_users.json"] - - def setUp(self): - super(AliasTestCase, self).setUp() - factories.populate_database() - - def test_alias(self): - user = User.objects.get(username="user@test.com") - values = dict( - username="user@test.com", role=user.group, - is_active=user.is_active, email="user@test.com", - aliases="toto@test.com", aliases_1="titi@test.com" - ) - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2) - - del values["aliases_1"] - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 1) - - def test_upper_case_alias(self): - """Try to create an upper case alias.""" - user = User.objects.get(username="user@test.com") - values = dict( - username="user@test.com", role=user.group, - is_active=user.is_active, email="user@test.com", - aliases="Toto@test.com" - ) - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self.assertEqual(user.mailbox_set.first().alias_set.count(), 1) - self.assertEqual( - user.mailbox_set.first().alias_set.first().full_address, - "toto@test.com" - ) - - values = { - "email": "Titi@test.com", "recipients": "user@test.com", - "enabled": True - } - self.ajax_post(reverse("admin:alias_add"), values) - self.assertEqual( - user.mailbox_set.first().alias_set.first().full_address, - "titi@test.com" - ) - - def test_dlist(self): - values = dict(email="all@test.com", - recipients="user@test.com", - recipients_1="admin@test.com", - recipients_2="ext@titi.com", - enabled=True) - self.ajax_post( - reverse("admin:dlist_add"), values - ) - user = User.objects.get(username="user@test.com") - self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2) - admin = User.objects.get(username="admin@test.com") - self.assertEqual(admin.mailbox_set.all()[0].alias_set.count(), 1) - - dlist = Alias.objects.get(address="all", domain__name="test.com") - self.assertEqual(len(dlist.get_recipients()), 3) - del values["recipients_1"] - self.ajax_post( - reverse("admin:alias_change", args=[dlist.id]), - values - ) - self.assertEqual(dlist.get_recipients_count(), 2) - - self.ajax_post( - reverse("admin:alias_delete") + "?selection=%d" % dlist.id, {} - ) - self.assertRaises(Alias.DoesNotExist, Alias.objects.get, - address="all", domain__name="test.com") - - def test_forward(self): - values = dict(email="forward2@test.com", recipients="rcpt@dest.com") - self.ajax_post( - reverse("admin:forward_add"), values - ) - fwd = Alias.objects.get(address="forward2", domain__name="test.com") - self.assertEqual(fwd.get_recipients_count(), 1) - - values["recipients"] = "rcpt2@dest.com" - self.ajax_post( - reverse("admin:alias_change", - args=[fwd.id]), - values - ) - self.assertEqual(fwd.get_recipients_count(), 1) - - self.ajax_post( - reverse("admin:alias_delete") + "?selection=%d" - % fwd.id, {} - ) - self.assertRaises(Alias.DoesNotExist, Alias.objects.get, - address="forward2", domain__name="test.com") - - def test_forward_and_local_copies(self): - values = dict(email="user@test.com", recipients="rcpt@dest.com") - self.ajax_post( - reverse("admin:forward_add"), values - ) - fwd = Alias.objects.get(address="user", domain__name="test.com") - self.assertEqual(fwd.get_recipients_count(), 1) - - values["recipients"] = "rcpt@dest.com" - values["recipients_1"] = "user@test.com" - self.ajax_post( - reverse("admin:alias_change", args=[fwd.id]), - values - ) - fwd = Alias.objects.get(pk=fwd.pk) - self.assertEqual(fwd.get_recipients_count(), 2) - self.assertEqual(fwd.aliases.count(), 0) diff --git a/modoboa/extensions/admin/tests/domain.py b/modoboa/extensions/admin/tests/domain.py deleted file mode 100644 index 3c6cd739a..000000000 --- a/modoboa/extensions/admin/tests/domain.py +++ /dev/null @@ -1,112 +0,0 @@ -from django.core.urlresolvers import reverse -from modoboa.core.models import User -from modoboa.lib import parameters -from modoboa.lib.tests import ModoTestCase -from modoboa.extensions.admin.models import ( - Domain, Alias -) -from modoboa.extensions.admin import factories - - -class DomainTestCase(ModoTestCase): - fixtures = ["initial_users.json"] - - def setUp(self): - super(DomainTestCase, self).setUp() - factories.populate_database() - - def test_create(self): - """Test the creation of a domain - """ - values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "no", - "stepid": 'step2' - } - self.ajax_post(reverse("admin:domain_add"), values) - dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.name, "pouet.com") - self.assertEqual(dom.quota, 100) - self.assertEqual(dom.enabled, False) - self.assertFalse(dom.admins) - - def test_create_with_template(self): - """Test the creation of a domain with a template - - """ - values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", - "stepid": 'step2' - } - self.ajax_post( - reverse("admin:domain_add"), - values - ) - dom = Domain.objects.get(name="pouet.com") - da = User.objects.get(username="toto@pouet.com") - self.assertIn(da, dom.admins) - al = Alias.objects.get(address="postmaster", domain__name="pouet.com") - self.assertIn(da.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(da.can_access(al)) - - def test_create_with_template_and_empty_quota(self): - """Test the creation of a domain with a template and no quota""" - values = { - "name": "pouet.com", "quota": 0, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", - "stepid": 'step2' - } - self.ajax_post( - reverse("admin:domain_add"), - values - ) - dom = Domain.objects.get(name="pouet.com") - da = User.objects.get(username="toto@pouet.com") - self.assertIn(da, dom.admins) - al = Alias.objects.get(address="postmaster", domain__name="pouet.com") - self.assertIn(da.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(da.can_access(al)) - - def test_create_using_default_quota(self): - parameters.save_admin('DEFAULT_DOMAIN_QUOTA', 50, app='admin') - values = { - "name": "pouet.com", "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", - "stepid": 'step2' - } - self.ajax_post( - reverse("admin:domain_add"), - values - ) - dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.quota, 50) - da = User.objects.get(username="toto@pouet.com") - self.assertEqual(da.mailbox_set.all()[0].quota, 50) - - def test_modify(self): - """Test the modification of a domain - - Rename 'test.com' domain to 'pouet.com' - """ - values = { - "name": "pouet.com", "quota": 100, "enabled": True - } - dom = Domain.objects.get(name="test.com") - self.ajax_post( - reverse("admin:domain_change", args=[dom.id]), - values - ) - dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.name, "pouet.com") - self.assertTrue(dom.enabled) - - def test_delete(self): - """Test the removal of a domain - """ - dom = Domain.objects.get(name="test.com") - self.ajax_post( - reverse("admin:domain_delete", args=[dom.id]), - {} - ) - with self.assertRaises(Domain.DoesNotExist): - Domain.objects.get(pk=1) diff --git a/modoboa/extensions/admin/tests/domain_alias.py b/modoboa/extensions/admin/tests/domain_alias.py deleted file mode 100644 index c4a1e5538..000000000 --- a/modoboa/extensions/admin/tests/domain_alias.py +++ /dev/null @@ -1,48 +0,0 @@ -from django.core.urlresolvers import reverse -from modoboa.lib.tests import ModoTestCase -from modoboa.extensions.admin.models import ( - Domain, DomainAlias -) -from modoboa.extensions.admin import factories - - -class DomainAliasTestCase(ModoTestCase): - fixtures = ['initial_users.json'] - - def setUp(self): - super(DomainAliasTestCase, self).setUp() - factories.populate_database() - self.dom = Domain.objects.get(name='test.com') - - def test_model(self): - dom = Domain.objects.get(name="test.com") - domal = DomainAlias() - domal.name = "domalias.net" - domal.target = dom - domal.enabled = True - domal.save() - self.assertEqual(dom.domainalias_count, 1) - - domal.name = "domalias.org" - domal.save() - - domal.delete() - - def test_form(self): - dom = Domain.objects.get(name="test.com") - values = dict(name=dom.name, quota=dom.quota, enabled=dom.enabled, - aliases="domalias.net", aliases_1="domalias.com") - self.ajax_post( - reverse("admin:domain_change", - args=[dom.id]), - values - ) - self.assertEqual(dom.domainalias_set.count(), 2) - - del values["aliases_1"] - self.ajax_post( - reverse("admin:domain_change", - args=[dom.id]), - values - ) - self.assertEqual(dom.domainalias_set.count(), 1) diff --git a/modoboa/extensions/admin/tests/export.py b/modoboa/extensions/admin/tests/export.py deleted file mode 100644 index ca95c4b7d..000000000 --- a/modoboa/extensions/admin/tests/export.py +++ /dev/null @@ -1,84 +0,0 @@ -from django.core.urlresolvers import reverse - -from modoboa.extensions.admin import factories -from modoboa.lib.tests import ModoTestCase - - -class ExportTestCase(ModoTestCase): - fixtures = ["initial_users.json"] - - def setUp(self): - super(ExportTestCase, self).setUp() - factories.populate_database() - - def __export_identities(self, idtfilter="", grpfilter=""): - self.clt.get( - reverse("admin:_identity_list") - + "?grpfilter=%s&idtfilter=%s" % (grpfilter, idtfilter) - ) - return self.clt.post( - reverse("admin:identity_export"), - {"filename": "test.csv"} - ) - - def assertListEqual(self, list1, list2): - list2 = list2.split('\r\n') - for entry in list1.split('\r\n'): - if not entry: - continue - self.assertIn(entry, list2) - - def test_export_identities(self): - response = self.__export_identities() - self.assertListEqual( - "account;admin;{CRYPT}dTTsGDkA5ZHKg;;;True;SuperAdmins;;\r\naccount;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\nalias;alias@test.com;True;user@test.com\r\nforward;forward@test.com;True;user@external.com\r\ndlist;postmaster@test.com;True;toto@titi.com;test@truc.fr\r\n", - response.content.strip() - ) - - def test_export_simpleusers(self): - response = self.__export_identities( - idtfilter="account", grpfilter="SimpleUsers" - ) - self.assertListEqual( - "account;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10", - response.content.strip() - ) - - def test_export_superadmins(self): - response = self.__export_identities( - idtfilter="account", grpfilter="SuperAdmins" - ) - self.assertEqual( - response.content.strip(), - "account;admin;{CRYPT}dTTsGDkA5ZHKg;;;True;SuperAdmins;;" - ) - - def test_export_domainadmins(self): - response = self.__export_identities( - idtfilter="account", grpfilter="DomainAdmins" - ) - self.assertListEqual( - "account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com", - response.content.strip() - ) - - def test_export_aliases(self): - response = self.__export_identities(idtfilter="alias") - self.assertEqual( - response.content.strip(), - "alias;alias@test.com;True;user@test.com" - ) - - def test_export_forwards(self): - response = self.__export_identities(idtfilter="forward") - self.assertEqual( - response.content.strip(), - "forward;forward@test.com;True;user@external.com" - ) - - def test_export_dlists(self): - response = self.__export_identities(idtfilter="dlist") - self.assertEqual( - response.content.strip(), - "dlist;postmaster@test.com;True;toto@titi.com;test@truc.fr" - ) diff --git a/modoboa/extensions/admin/tests/import_.py b/modoboa/extensions/admin/tests/import_.py deleted file mode 100644 index dee11d995..000000000 --- a/modoboa/extensions/admin/tests/import_.py +++ /dev/null @@ -1,191 +0,0 @@ -# coding: utf-8 -from django.core.files.base import ContentFile -from django.core.urlresolvers import reverse - -from modoboa.core.models import User -from modoboa.extensions.admin import factories -from modoboa.extensions.admin.models import ( - Domain, Alias, DomainAlias -) -from modoboa.lib.tests import ModoTestCase - - -class ImportTestCase(ModoTestCase): - fixtures = ["initial_users.json"] - - def setUp(self): - super(ImportTestCase, self).setUp() - factories.populate_database() - - def test_domains_import(self): - f = ContentFile(b"""domain; domain1.com; 100; True -domain; domain2.com; 200; False -domainalias; domalias1.com; domain1.com; True -""", name="domains.csv") - self.clt.post( - reverse("admin:domain_import"), { - "sourcefile": f - } - ) - admin = User.objects.get(username="admin") - dom = Domain.objects.get(name="domain1.com") - self.assertEqual(dom.quota, 100) - self.assertTrue(dom.enabled) - self.assertTrue(admin.is_owner(dom)) - domalias = DomainAlias.objects.get(name="domalias1.com") - self.assertEqual(domalias.target, dom) - self.assertTrue(dom.enabled) - self.assertTrue(admin.is_owner(domalias)) - dom = Domain.objects.get(name="domain2.com") - self.assertEqual(dom.quota, 200) - self.assertFalse(dom.enabled) - self.assertTrue(admin.is_owner(dom)) - - def test_import_domains_with_conflict(self): - f = ContentFile(b"""domain;test.alias;10;True -domainalias;test.alias;test.com;True -""", name="domains.csv") - resp = self.clt.post( - reverse("admin:domain_import"), { - "sourcefile": f - } - ) - self.assertIn('Object already exists: domainalias', resp.content) - - def test_identities_import(self): - f = ContentFile(b""" -account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0 -account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com -alias; alias1@test.com; True; user1@test.com -forward; fwd1@test.com; True; user@extdomain.com -dlist; dlist@test.com; True; user1@test.com; user@extdomain.com -""", name="identities.csv") - self.clt.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} - ) - admin = User.objects.get(username="admin") - u1 = User.objects.get(username="user1@test.com") - mb1 = u1.mailbox_set.all()[0] - self.assertTrue(admin.is_owner(u1)) - self.assertEqual(u1.email, "user1@test.com") - self.assertEqual(u1.first_name, "User") - self.assertEqual(u1.last_name, "One") - self.assertTrue(u1.is_active) - self.assertEqual(u1.group, "SimpleUsers") - self.assertTrue(mb1.use_domain_quota) - self.assertEqual(mb1.quota, 0) - self.assertTrue(admin.is_owner(mb1)) - self.assertEqual(mb1.full_address, "user1@test.com") - self.assertTrue( - self.clt.login(username="user1@test.com", password="toto") - ) - - da = User.objects.get(username="truc@test.com") - damb = da.mailbox_set.all()[0] - self.assertEqual(da.first_name, u"René") - self.assertEqual(da.group, "DomainAdmins") - self.assertEqual(damb.quota, 5) - self.assertFalse(damb.use_domain_quota) - self.assertEqual(damb.full_address, "truc@test.com") - dom = Domain.objects.get(name="test.com") - self.assertIn(da, dom.admins) - u = User.objects.get(username="user@test.com") - self.assertTrue(da.can_access(u)) - - al = Alias.objects.get(address="alias1", domain__name="test.com") - self.assertIn(u1.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(admin.is_owner(al)) - - fwd = Alias.objects.get(address="fwd1", domain__name="test.com") - self.assertIn("user@extdomain.com", fwd.extmboxes) - self.assertTrue(admin.is_owner(fwd)) - - dlist = Alias.objects.get(address="dlist", domain__name="test.com") - self.assertIn(u1.mailbox_set.all()[0], dlist.mboxes.all()) - self.assertIn("user@extdomain.com", dlist.extmboxes) - self.assertTrue(admin.is_owner(dlist)) - - def test_import_invalid_quota(self): - f = ContentFile(b""" -account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; ; test.com -""", name="identities.csv") - resp = self.clt.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} - ) - self.assertIn('wrong quota value', resp.content) - - def test_import_quota_too_big(self): - self.clt.logout() - self.clt.login(username="admin@test.com", password="toto") - f = ContentFile(b""" -account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 20 -""", name="identities.csv") - resp = self.clt.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} - ) - self.assertIn('Quota is greater than the allowed', resp.content) - - def test_import_missing_quota(self): - f = ContentFile(b""" -account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com -""", name="identities.csv") - resp = self.clt.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} - ) - account = User.objects.get(username="user1@test.com") - self.assertEqual( - account.mailbox_set.all()[0].quota, - account.mailbox_set.all()[0].domain.quota - ) - - def test_import_duplicate(self): - f = ContentFile(b""" -account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; 0; test.com -account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0; test.com -""", name="identities.csv") - self.clt.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True, - "continue_if_exists": True} - ) - admin = User.objects.get(username="admin") - u1 = User.objects.get(username="truc@test.com") - self.assertTrue(admin.is_owner(u1)) - - def test_import_superadmin(self): - """Check if a domain admin can import a superadmin - - Expected result: no - """ - self.clt.logout() - self.assertTrue( - self.clt.login(username="admin@test.com", password="toto") - ) - f = ContentFile(b""" -account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com; 50 -""", name="identities.csv") - self.clt.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True, - "continue_if_exists": True} - ) - with self.assertRaises(User.DoesNotExist): - User.objects.get(username="sa@test.com") - - def test_import_alias_with_empty_values(self): - f = ContentFile(b""" -alias;user.alias@test.com;True;user@test.com;;;;;;;;;;;;;;;; -""", name="identities.csv") - self.clt.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True, - "continue_if_exists": True} - ) - alias = Alias.objects.get( - domain__name="test.com", address="user.alias" - ) - self.assertEqual(alias.type, "alias") diff --git a/modoboa/extensions/admin/tests/password_schemes.py b/modoboa/extensions/admin/tests/password_schemes.py deleted file mode 100644 index 413d43651..000000000 --- a/modoboa/extensions/admin/tests/password_schemes.py +++ /dev/null @@ -1,55 +0,0 @@ -from django.core.urlresolvers import reverse -from modoboa.core import load_core_settings -from modoboa.core.models import User -from modoboa.lib.tests import ModoTestCase -from modoboa.lib import parameters -from modoboa.extensions.admin import factories - - -class PasswordSchemesTestCase(ModoTestCase): - fixtures = ['initial_users.json'] - - def setUp(self): - super(PasswordSchemesTestCase, self).setUp() - factories.populate_database() - load_core_settings() - - def _create_account(self): - values = dict( - username="tester@test.com", first_name="Tester", last_name="Toto", - password1="toto", password2="toto", role="SimpleUsers", - quota_act=True, - is_active=True, email="tester@test.com", stepid='step2' - ) - self.ajax_post( - reverse("admin:account_add"), - values - ) - - def _test_scheme(self, name, startpattern): - parameters.save_admin('PASSWORD_SCHEME', name, app='core') - self._create_account() - account = User.objects.get(username='tester@test.com') - self.assertTrue(account.password.startswith(startpattern)) - self.assertTrue(account.check_password('toto')) - - def test_sha512crypt_scheme(self): - self._test_scheme('sha512crypt', '{SHA512-CRYPT}') - - def test_sha256crypt_scheme(self): - self._test_scheme('sha256crypt', '{SHA256-CRYPT}') - - def test_md5crypt_scheme(self): - self._test_scheme('md5crypt', '{MD5-CRYPT}') - - def test_sha256_scheme(self): - self._test_scheme('sha256', '{SHA256}') - - def test_md5_scheme(self): - self._test_scheme('md5', '{MD5}') - - def test_crypt(self): - self._test_scheme('crypt', '{CRYPT}') - - def test_plain(self): - self._test_scheme('plain', '{PLAIN}') diff --git a/modoboa/extensions/admin/tests/user.py b/modoboa/extensions/admin/tests/user.py deleted file mode 100644 index 173d03026..000000000 --- a/modoboa/extensions/admin/tests/user.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.core.urlresolvers import reverse -from modoboa.core.models import User -from modoboa.lib.tests import ModoTestCase -from modoboa.extensions.admin.factories import populate_database -from modoboa.extensions.admin.models import Alias - - -class ForwardTestCase(ModoTestCase): - fixtures = ['initial_users.json'] - - def setUp(self): - super(ForwardTestCase, self).setUp() - populate_database() - - def test_forward_permissions(self): - self.clt.logout() - self.clt.login(username='user@test.com', password='toto') - self.ajax_post( - reverse('user_forward'), - {'dest': 'user@extdomain.com', 'keepcopies': True} - ) - forward = Alias.objects.get(address='user', domain__name='test.com') - sadmin = User.objects.get(username='admin') - self.assertTrue(sadmin.can_access(forward)) - domadmin = User.objects.get(username='admin@test.com') - self.assertTrue(domadmin.can_access(forward)) diff --git a/modoboa/extensions/admin/urls.py b/modoboa/extensions/admin/urls.py deleted file mode 100644 index e601c45d1..000000000 --- a/modoboa/extensions/admin/urls.py +++ /dev/null @@ -1,60 +0,0 @@ -from django.conf.urls import patterns, url - - -urlpatterns = patterns( - 'modoboa.extensions.admin.views.domain', - - url(r'^$', 'index', name="index"), - url(r'^domains/$', 'domains', name="domain_list"), - url(r'^domains/list/$', '_domains', name="_domain_list"), - url(r'^domains/flatlist/$', 'domains_list', name="domain_flat_list"), - url(r'^domains/stats/$', 'domain_statistics', name="domain_statistics"), - url(r'^domains/new/', 'newdomain', name='domain_add'), - url(r'^domains/(?P\d+)/edit/$', 'editdomain', name="domain_change"), - url(r'^domains/(?P\d+)/delete/$', 'deldomain', - name="domain_delete"), -) - -urlpatterns += patterns( - 'modoboa.extensions.admin.views.identity', - - url(r'^permissions/remove/$', 'remove_permission', - name="permission_remove"), - - url(r'^identities/$', 'identities', name="identity_list"), - url(r'^identities/list/$', '_identities', name="_identity_list"), - url(r'^identities/quotas/$', 'list_quotas', name="quota_list"), - url(r'^identities/page/$', 'get_next_page', name="identity_page"), - - url(r'^accounts/list/$', 'accounts_list', name="account_list"), - url(r'^accounts/new/$', 'newaccount', name="account_add"), - url(r'^accounts/edit/(?P\d+)/$', 'editaccount', - name="account_change"), - url(r'^accounts/delete/(?P\d+)/$', 'delaccount', - name="account_delete"), -) - -urlpatterns += patterns( - 'modoboa.extensions.admin.views.alias', - - url(r'^distriblists/new/$', 'newdlist', name="dlist_add"), - url(r'^forwards/new/$', 'newforward', name="forward_add"), - - url(r'^aliases/new/$', 'newalias', name="alias_add"), - url(r'^aliases/edit/(?P\d+)/$', 'editalias', name="alias_change"), - url(r'^aliases/delete/$', 'delalias', name="alias_delete"), -) - -urlpatterns += patterns( - 'modoboa.extensions.admin.views.import', - - url(r'^domains/import/$', 'import_domains', name="domain_import"), - url(r'^identities/import/$', 'import_identities', name="identity_import"), -) - -urlpatterns += patterns( - 'modoboa.extensions.admin.views.export', - - url(r'^domains/export/$', 'export_domains', name="domain_export"), - url(r'^identities/export/$', 'export_identities', name="identity_export"), -) diff --git a/modoboa/extensions/admin/views/__init__.py b/modoboa/extensions/admin/views/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/admin/views/alias.py b/modoboa/extensions/admin/views/alias.py deleted file mode 100644 index 0104b667a..000000000 --- a/modoboa/extensions/admin/views/alias.py +++ /dev/null @@ -1,135 +0,0 @@ -import reversion -from django.db import IntegrityError -from django.utils.translation import ugettext as _, ungettext -from django.shortcuts import render -from django.core.urlresolvers import reverse -from django.contrib.auth.decorators import ( - login_required, permission_required -) -from modoboa.lib import events -from modoboa.lib.web_utils import render_to_json_response -from modoboa.lib.exceptions import PermDeniedException, Conflict -from modoboa.extensions.admin.forms import AliasForm -from modoboa.extensions.admin.models import Alias - - -def _validate_alias(request, form, successmsg, callback=None): - """Alias validation - - Common function shared between creation and modification actions. - """ - if form.is_valid(): - form.set_recipients() - try: - alias = form.save() - except IntegrityError: - raise Conflict(_("Alias with this name already exists")) - if callback: - callback(request.user, alias) - return render_to_json_response(successmsg) - - return render_to_json_response({'form_errors': form.errors}, status=400) - - -def _new_alias(request, title, action, successmsg, - tplname="admin/aliasform.html"): - events.raiseEvent("CanCreate", request.user, "mailbox_aliases") - if request.method == "POST": - def callback(user, alias): - alias.post_create(user) - - form = AliasForm(request.user, request.POST) - return _validate_alias( - request, form, successmsg, callback - ) - - ctx = { - "title": title, - "action": action, - "formid": "aliasform", - "action_label": _("Create"), - "action_classes": "submit", - "form": AliasForm(request.user) - } - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.add_alias") -@reversion.create_revision() -def newdlist(request): - return _new_alias( - request, _("New distribution list"), reverse("admin:dlist_add"), - _("Distribution list created") - ) - - -@login_required -@permission_required("admin.add_alias") -@reversion.create_revision() -def newalias(request): - return _new_alias( - request, _("New alias"), reverse("admin:alias_add"), - _("Alias created") - ) - - -@login_required -@permission_required("admin.add_alias") -@reversion.create_revision() -def newforward(request): - return _new_alias( - request, _("New forward"), reverse("admin:forward_add"), - _("Forward created") - ) - - -@login_required -@permission_required("admin.change_alias") -@reversion.create_revision() -def editalias(request, alid, tplname="admin/aliasform.html"): - alias = Alias.objects.get(pk=alid) - if not request.user.can_access(alias): - raise PermDeniedException - if request.method == "POST": - if len(alias.get_recipients()) >= 2: - successmsg = _("Distribution list modified") - elif alias.extmboxes != "": - successmsg = _("Forward modified") - else: - successmsg = _("Alias modified") - form = AliasForm(request.user, request.POST, instance=alias) - return _validate_alias(request, form, successmsg) - - ctx = { - 'action': reverse("admin:alias_change", args=[alias.id]), - 'formid': 'aliasform', - 'title': alias.full_address, - 'action_label': _('Update'), - 'action_classes': 'submit', - 'form': AliasForm(request.user, instance=alias) - } - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.delete_alias") -def delalias(request): - selection = request.GET["selection"].split(",") - for alid in selection: - alias = Alias.objects.get(pk=alid) - if not request.user.can_access(alias): - raise PermDeniedException - if alias.type == 'dlist': - msg = "Distribution list deleted" - msgs = "Distribution lists deleted" - elif alias.type == 'forward': - msg = "Forward deleted" - msgs = "Forwards deleted" - else: - msg = "Alias deleted" - msgs = "Aliases deleted" - alias.delete() - - msg = ungettext(msg, msgs, len(selection)) - return render_to_json_response(msg) diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py deleted file mode 100644 index 35ef6a68d..000000000 --- a/modoboa/extensions/admin/views/domain.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Domain related views. -""" - -from django.http import HttpResponseRedirect -from django.utils.translation import ugettext as _, ungettext -from django.core.urlresolvers import reverse -from django.shortcuts import render -from django.views.decorators.csrf import ensure_csrf_cookie -from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test -) - -import reversion - -from modoboa.core.models import User -from modoboa.lib import parameters, events -from modoboa.lib.web_utils import ( - _render_to_string, render_to_json_response -) -from modoboa.lib.exceptions import ( - PermDeniedException -) -from modoboa.lib.listing import get_sort_order, get_listing_page -from modoboa.extensions.admin.models import ( - Domain, DomainAlias, Mailbox, Alias -) -from modoboa.extensions.admin.forms import ( - DomainForm -) -from modoboa.extensions.admin.lib import get_domains - - -@login_required -def index(request): - return HttpResponseRedirect(reverse("admin:domain_list")) - - -@login_required -@user_passes_test( - lambda u: u.has_perm("admin.view_domains") - or u.has_perm("admin.view_mailboxes") -) -def _domains(request): - sort_order, sort_dir = get_sort_order(request.GET, "name") - filters = dict( - (flt, request.GET.get(flt, None)) - for flt in ['domfilter', 'searchquery'] - + events.raiseQueryEvent('ExtraDomainFilters') - ) - request.session['domains_filters'] = filters - domainlist = get_domains(request.user, **filters) - if sort_order == 'name': - domainlist = sorted( - domainlist, - key=lambda d: getattr(d, sort_order), reverse=sort_dir == '-' - ) - else: - domainlist = sorted(domainlist, key=lambda d: d.tags[0], - reverse=sort_dir == '-') - context = { - "handle_mailboxes": parameters.get_admin( - "HANDLE_MAILBOXES", raise_error=False), - "auto_account_removal": parameters.get_admin("AUTO_ACCOUNT_REMOVAL") - } - page = get_listing_page(domainlist, request.GET.get("page", 1)) - if page is None: - context["length"] = 0 - else: - context["rows"] = _render_to_string( - request, 'admin/domains_table.html', { - 'domains': page.object_list, - } - ) - context["pages"] = [page.number] - return render_to_json_response(context) - - -@login_required -@ensure_csrf_cookie -def domains(request, tplname="admin/domains.html"): - if not request.user.has_perm("admin.view_domains"): - if request.user.has_perm("admin.view_mailboxes"): - return HttpResponseRedirect( - reverse("admin:identity_list") - ) - return HttpResponseRedirect(reverse("core:user_index")) - return render(request, tplname, {"selection": "domains"}) - - -@login_required -@permission_required("core.add_user") -def domains_list(request): - doms = [dom.name for dom in Domain.objects.get_for_admin(request.user)] - return render_to_json_response(doms) - - -@login_required -@permission_required("admin.add_domain") -@reversion.create_revision() -def newdomain(request): - from modoboa.extensions.admin.forms import DomainWizard - - events.raiseEvent("CanCreate", request.user, "domains") - return DomainWizard(request).process() - - -@login_required -@permission_required("admin.view_domains") -@reversion.create_revision() -def editdomain(request, dom_id): - """Edit domain view.""" - domain = Domain.objects.get(pk=dom_id) - if not request.user.can_access(domain): - raise PermDeniedException - - instances = dict(general=domain) - events.raiseEvent("FillDomainInstances", request.user, domain, instances) - return DomainForm(request, instances=instances).process() - - -@login_required -@permission_required("admin.delete_domain") -def deldomain(request, dom_id): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - try: - mb = Mailbox.objects.get(user__id=request.user.id) - except Mailbox.DoesNotExist: - mb = None - - dom = Domain.objects.get(pk=dom_id) - if not request.user.can_access(dom): - raise PermDeniedException - if mb and mb.domain == dom: - raise PermDeniedException(_("You can't delete your own domain")) - dom.delete(request.user, keepdir) - - msg = ungettext("Domain deleted", "Domains deleted", 1) - return render_to_json_response(msg) - - -@login_required -@permission_required("admin.view_domains") -def domain_statistics(request): - """A simple page to present domain statistics.""" - context = {} - if request.user.group == "SuperAdmins": - context.update({ - "domains_counter": Domain.objects.count(), - "domain_aliases_counter": DomainAlias.objects.count(), - "identities_counter": User.objects.count() + Alias.objects.count(), - }) - context.update({"domains": Domain.objects.get_for_admin(request.user)}) - return render(request, "admin/domain_statistics.html", context) diff --git a/modoboa/extensions/admin/views/export.py b/modoboa/extensions/admin/views/export.py deleted file mode 100644 index 38fe6494f..000000000 --- a/modoboa/extensions/admin/views/export.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Export related views.""" - -import cStringIO -import csv - -from rfc6266 import build_header - -from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test -) -from django.core.urlresolvers import reverse -from django.http import HttpResponse -from django.shortcuts import render -from django.utils.translation import ugettext as _ - -from modoboa.extensions.admin.forms import ( - ExportIdentitiesForm, ExportDomainsForm -) -from modoboa.extensions.admin.lib import get_domains, get_identities - - -def _export(content, filename): - """Export a csv file's content - - :param content: the content to export (string) - :param filename: the name that will appear into the response - :return: an ``HttpResponse`` object - """ - resp = HttpResponse(content) - resp["Content-Type"] = "text/csv" - resp["Content-Length"] = len(content) - resp["Content-Disposition"] = build_header(filename) - return resp - - -@login_required -@user_passes_test( - lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") -) -def export_identities(request): - ctx = { - "title": _("Export identities"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse("admin:identity_export"), - } - - if request.method == "POST": - form = ExportIdentitiesForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - identities = get_identities( - request.user, **request.session['identities_filters']) - for ident in identities: - ident.to_csv(csvwriter) - content = fp.getvalue() - fp.close() - return _export(content, form.cleaned_data["filename"]) - - ctx["form"] = ExportIdentitiesForm() - return render(request, "common/generic_modal_form.html", ctx) - - -@login_required -@permission_required("admin.add_domain") -def export_domains(request): - ctx = { - "title": _("Export domains"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse("admin:domain_export"), - } - - if request.method == "POST": - form = ExportDomainsForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for dom in get_domains(request.user, - **request.session['domains_filters']): - dom.to_csv(csvwriter) - content = fp.getvalue() - fp.close() - return _export(content, form.cleaned_data["filename"]) - - ctx["form"] = ExportDomainsForm() - return render(request, "common/generic_modal_form.html", ctx) diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py deleted file mode 100644 index cf7898114..000000000 --- a/modoboa/extensions/admin/views/identity.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Identity related views.""" - -from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test -) -from django.shortcuts import render -from django.utils.translation import ugettext as _, ungettext -from django.views.decorators.csrf import ensure_csrf_cookie - -import reversion - -from modoboa.core.models import User -from modoboa.extensions.admin.forms import ( - AccountForm -) -from modoboa.extensions.admin.lib import get_identities -from modoboa.extensions.admin.models import Mailbox, Domain -from modoboa.lib import parameters, events -from modoboa.lib.exceptions import ( - PermDeniedException, BadRequest -) -from modoboa.lib.listing import ( - get_sort_order, get_listing_page -) -from modoboa.lib.web_utils import ( - _render_to_string, render_to_json_response -) - - -@login_required -@user_passes_test( - lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") -) -def _identities(request): - filters = dict((fname, request.GET.get(fname, None)) - for fname in ['searchquery', 'idtfilter', 'grpfilter']) - request.session['identities_filters'] = filters - idents_list = get_identities(request.user, **filters) - sort_order, sort_dir = get_sort_order(request.GET, "identity", - ["identity", "name_or_rcpt", "tags"]) - if sort_order in ["identity", "name_or_rcpt"]: - objects = sorted(idents_list, key=lambda o: getattr(o, sort_order), - reverse=sort_dir == '-') - else: - objects = sorted(idents_list, key=lambda o: o.tags[0], - reverse=sort_dir == '-') - context = { - "handle_mailboxes": parameters.get_admin( - "HANDLE_MAILBOXES", raise_error=False) - } - page = get_listing_page(objects, request.GET.get("page", 1)) - if page is None: - context["length"] = 0 - else: - context["headers"] = _render_to_string( - request, "admin/identity_headers.html", {}) - context["rows"] = _render_to_string( - request, "admin/identities_table.html", { - "identities": page.object_list - } - ) - context["pages"] = [page.number] - return render_to_json_response(context) - - -@login_required -@permission_required("admin.add_mailbox") -def list_quotas(request): - from modoboa.lib.db_utils import db_type - - sort_order, sort_dir = get_sort_order(request.GET, "address") - mboxes = Mailbox.objects.get_for_admin( - request.user, request.GET.get("searchquery", None) - ) - mboxes = mboxes.exclude(quota=0) - if sort_order in ["address", "quota"]: - mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order)) - elif sort_order == "quota_value__bytes": - where = "admin_mailbox.address||'@'||admin_domain.name" - mboxes = mboxes.extra( - select={"quota_value__bytes": "admin_quota.bytes"}, - where=["admin_quota.username=%s" % where], - tables=["admin_quota", "admin_domain"], - order_by=["%s%s" % (sort_dir, sort_order)] - ) - elif sort_order == "quota_usage": - where = "admin_mailbox.address||'@'||admin_domain.name" - db_type = db_type() - if db_type == "postgres": - select = ( - "(admin_quota.bytes::float / (CAST(admin_mailbox.quota " - "AS BIGINT) * 1048576)) * 100" - ) - else: - select = ( - "admin_quota.bytes / (admin_mailbox.quota " - "* 1048576) * 100" - ) - if db_type == "mysql": - where = "CONCAT(admin_mailbox.address,'@',admin_domain.name)" - mboxes = mboxes.extra( - select={'quota_usage': select}, - where=["admin_quota.username=%s" % where], - tables=["admin_quota", "admin_domain"], - order_by=["%s%s" % (sort_dir, sort_order)] - ) - else: - raise BadRequest(_("Invalid request")) - page = get_listing_page(mboxes, request.GET.get("page", 1)) - context = { - "headers": _render_to_string( - request, "admin/quota_headers.html", {} - ) - } - if page is None: - context["length"] = 0 - else: - context["rows"] = _render_to_string( - request, "admin/quotas.html", { - "mboxes": page - } - ) - context["pages"] = [page.number] - return render_to_json_response(context) - - -@login_required -@user_passes_test( - lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias") - or u.has_perm("admin.add_mailbox") -) -def get_next_page(request): - """Return the next page of the identity list.""" - if request.GET.get("objtype", "identity") == "identity": - return _identities(request) - return list_quotas(request) - - -@login_required -@user_passes_test( - lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias") -) -@ensure_csrf_cookie -def identities(request, tplname="admin/identities.html"): - return render(request, tplname, { - "selection": "identities", - "deflocation": "list/" - }) - - -@login_required -@permission_required("core.add_user") -def accounts_list(request): - accs = User.objects.filter(is_superuser=False) \ - .exclude(groups__name='SimpleUsers') - res = [a.username for a in accs.all()] - return render_to_json_response(res) - - -@login_required -@permission_required("core.add_user") -@reversion.create_revision() -def newaccount(request): - """Create a new account.""" - from modoboa.extensions.admin.forms import AccountWizard - return AccountWizard(request).process() - - -@login_required -@permission_required("core.change_user") -@reversion.create_revision() -def editaccount(request, accountid, tplname="common/tabforms.html"): - account = User.objects.get(pk=accountid) - if not request.user.can_access(account): - raise PermDeniedException - mb = None - if account.mailbox_set.count(): - mb = account.mailbox_set.all()[0] - - instances = dict(general=account, mail=mb, perms=account) - events.raiseEvent("FillAccountInstances", request.user, account, instances) - return AccountForm(request, instances=instances).process() - - -@login_required -@permission_required("core.delete_user") -def delaccount(request, accountid): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - User.objects.get(pk=accountid).delete(request.user, keep_mb_dir=keepdir) - return render_to_json_response( - ungettext("Account deleted", "Accounts deleted", 1) - ) - - -@login_required -@permission_required("admin.add_domain") -def remove_permission(request): - domid = request.GET.get("domid", None) - daid = request.GET.get("daid", None) - if domid is None or daid is None: - raise BadRequest(_("Invalid request")) - try: - account = User.objects.get(pk=daid) - domain = Domain.objects.get(pk=domid) - except (User.DoesNotExist, Domain.DoesNotExist): - raise BadRequest(_("Invalid request")) - if not request.user.can_access(account) or \ - not request.user.can_access(domain): - raise PermDeniedException - domain.remove_admin(account) - return render_to_json_response({}) diff --git a/modoboa/extensions/admin/views/import.py b/modoboa/extensions/admin/views/import.py deleted file mode 100644 index 015db07d2..000000000 --- a/modoboa/extensions/admin/views/import.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Import related views.""" - -import csv - -from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test -) -from django.core.urlresolvers import reverse -from django.shortcuts import render -from django.utils.translation import ugettext as _ - -import reversion - -from modoboa.core.models import User -from modoboa.extensions.admin.forms import ImportIdentitiesForm, ImportDataForm -from modoboa.extensions.admin.models import ( - Domain, DomainAlias, Alias -) -from modoboa.lib import events -from modoboa.lib.exceptions import ModoboaException, Conflict - - -def import_domain(user, row, formopts): - """Specific code for domains import""" - dom = Domain() - dom.from_csv(user, row) - - -def import_domainalias(user, row, formopts): - """Specific code for domain aliases import""" - domalias = DomainAlias() - domalias.from_csv(user, row) - - -@login_required -@permission_required("admin.add_domain") -def import_domains(request): - if request.method == "POST": - return importdata(request) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -
      -
    • domain; name; quota; enabled
    • -
    • domainalias; name; targeted domain; enabled
    • - %s -
    -

    The first element of each line is mandatory and must be equal to one of the previous values.

    -

    You can use a different character as separator.

    -""" % ''.join([unicode(hlp) for hlp in - events.raiseQueryEvent('ExtraDomainImportHelp')])) - - ctx = dict( - title=_("Import domains"), - action_label=_("Import"), - action_classes="submit", - action=reverse("admin:domain_import"), - formid="importform", - enctype="multipart/form-data", - target="import_target", - helptext=helptext, - form=ImportDataForm() - ) - return render(request, "admin/importform.html", ctx) - - -def import_account(user, row, formopts): - """Specific code for accounts import""" - account = User() - account.from_csv(user, row, formopts["crypt_password"]) - - -def _import_alias(user, row, **kwargs): - """Specific code for aliases import""" - alias = Alias() - alias.from_csv(user, row, **kwargs) - - -def import_alias(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_forward(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_dlist(user, row, formopts): - _import_alias(user, row) - - -@reversion.create_revision() -def importdata(request, formclass=ImportDataForm): - """Generic import function - - As the process of importing data from a CSV file is the same - whatever the type, we do a maximum of the work here. - - :param request: a ``Request`` instance - :param typ: a string indicating the object type being imported - :return: a ``Response`` instance - """ - error = None - form = formclass(request.POST, request.FILES) - if form.is_valid(): - try: - reader = csv.reader(request.FILES['sourcefile'], - delimiter=form.cleaned_data['sepchar']) - except csv.Error as inst: - error = str(inst) - - if error is None: - try: - cpt = 0 - for row in reader: - if not row: - continue - try: - fct = globals()["import_%s" % row[0].strip()] - except KeyError: - fct = events.raiseQueryEvent( - 'ImportObject', row[0].strip() - ) - if not fct: - continue - fct = fct[0] - try: - fct(request.user, row, form.cleaned_data) - except Conflict: - if form.cleaned_data["continue_if_exists"]: - continue - raise Conflict( - _("Object already exists: %s" - % form.cleaned_data['sepchar'].join(row[:2])) - ) - cpt += 1 - msg = _("%d objects imported successfully" % cpt) - return render(request, "admin/import_done.html", { - "status": "ok", "msg": msg - }) - except (ModoboaException) as e: - error = str(e) - - return render(request, "admin/import_done.html", { - "status": "ko", "msg": error - }) - - -@login_required -@user_passes_test( - lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") -) -def import_identities(request): - if request.method == "POST": - return importdata(request, ImportIdentitiesForm) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -
      -
    • account; loginname; password; first name; last name; enabled; group; address; quota; [, domain, ...]
    • -
    • alias; address; enabled; internal recipient
    • -
    • forward; address; enabled; external recipient
    • -
    • dlist; address; enabled; recipient; recipient; ...
    • -
    -

    The first element of each line is mandatory and must be equal to one of the previous values.

    - -

    You can use a different character as separator.

    -""") - ctx = dict( - title=_("Import identities"), - action_label=_("Import"), - action_classes="submit", - action=reverse("admin:identity_import"), - formid="importform", - enctype="multipart/form-data", - target="import_target", - form=ImportIdentitiesForm(), - helptext=helptext - ) - return render(request, "admin/importform.html", ctx) diff --git a/modoboa/extensions/admin/views/user.py b/modoboa/extensions/admin/views/user.py deleted file mode 100644 index 55166166c..000000000 --- a/modoboa/extensions/admin/views/user.py +++ /dev/null @@ -1,57 +0,0 @@ -import reversion -from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext as _ -from modoboa.lib.web_utils import ( - render_to_json_response, _render_to_string -) -from modoboa.extensions.admin.lib import needs_mailbox -from modoboa.extensions.admin.models import Alias, Mailbox -from modoboa.extensions.admin.forms import ForwardForm - - -@login_required -@needs_mailbox() -@reversion.create_revision() -def forward(request, tplname='admin/forward.html'): - mb = request.user.mailbox_set.all()[0] - try: - al = Alias.objects.get(address=mb.address, - domain__name=mb.domain.name) - except Alias.DoesNotExist: - al = None - if request.method == "POST": - form = ForwardForm(request.POST) - error = None - if form.is_valid(): - if al is None: - al = Alias() - al.address = mb.address - al.domain = mb.domain - al.enabled = mb.user.is_active - intdests = [] - if form.cleaned_data["keepcopies"]: - intdests += [mb] - form.parse_dest() - al.save( - int_rcpts=intdests, ext_rcpts=form.dests, creator=request.user - ) - return render_to_json_response(_("Forward updated")) - - return render_to_json_response( - {'form_errors': form.errors}, status=400 - ) - - form = ForwardForm() - if al is not None: - form.fields["dest"].initial = al.extmboxes - try: - al.mboxes.get(pk=mb.id) - except Mailbox.DoesNotExist: - pass - else: - form.fields["keepcopies"].initial = True - return render_to_json_response({ - "content": _render_to_string(request, tplname, { - "form": form - }) - }) diff --git a/modoboa/extensions/amavis/__init__.py b/modoboa/extensions/amavis/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/amavis/app_settings.py b/modoboa/extensions/amavis/app_settings.py deleted file mode 100644 index 572d31d99..000000000 --- a/modoboa/extensions/amavis/app_settings.py +++ /dev/null @@ -1,162 +0,0 @@ -# coding: utf-8 - -from django import forms -from django.utils.translation import ugettext_lazy as _ - -from modoboa.lib.form_utils import SeparatorField, YesNoField, InlineRadioSelect -from modoboa.lib.parameters import AdminParametersForm, UserParametersForm - - -class ParametersForm(AdminParametersForm): - - """Extension settings.""" - - app = "amavis" - - qsettings_sep = SeparatorField(label=_("Quarantine settings")) - - max_messages_age = forms.IntegerField( - label=_("Maximum message age"), - initial=14, - help_text=_( - "Quarantine messages maximum age (in days) before deletion" - ) - ) - - sep1 = SeparatorField(label=_("Messages releasing")) - - released_msgs_cleanup = YesNoField( - label=_("Remove released messages"), - initial="no", - help_text=_("Remove messages marked as released while cleaning up " - "the database") - ) - - am_pdp_mode = forms.ChoiceField( - label=_("Amavis connection mode"), - choices=[("inet", "inet"), ("unix", "unix")], - initial="unix", - help_text=_("Mode used to access the PDP server"), - widget=InlineRadioSelect(attrs={"type": "checkbox"}) - ) - - am_pdp_host = forms.CharField( - label=_("PDP server address"), - initial="localhost", - help_text=_("PDP server address (if inet mode)"), - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - - am_pdp_port = forms.IntegerField( - label=_("PDP server port"), - initial=9998, - help_text=_("PDP server port (if inet mode)") - ) - - am_pdp_socket = forms.CharField( - label=_("PDP server socket"), - initial="/var/amavis/amavisd.sock", - help_text=_("Path to the PDP server socket (if unix mode)") - ) - - user_can_release = YesNoField( - label=_("Allow direct release"), - initial="no", - help_text=_("Allow users to directly release their messages") - ) - - self_service = YesNoField( - label=_("Enable self-service mode"), - initial="no", - help_text=_("Activate the 'self-service' mode") - ) - - notifications_sender = forms.EmailField( - label=_("Notifications sender"), - initial="notification@modoboa.org", - help_text=_("The e-mail address used to send notitications") - ) - - lsep = SeparatorField(label=_("Manual learning")) - - manual_learning = YesNoField( - label=_("Enable manual learning"), - initial="yes", - help_text=_( - "Allow super administrators to manually train Spamassassin" - ) - ) - - sa_is_local = YesNoField( - label=_("Is Spamassassin local?"), - initial="yes", - help_text=_( - "Tell if Spamassassin is running on the same server than modoboa" - ) - ) - - default_user = forms.CharField( - label=_("Default user"), - initial="amavis", - help_text=_( - "Name of the user owning the default bayesian database" - ) - ) - - spamd_address = forms.CharField( - label=_("Spamd address"), - initial="127.0.0.1", - help_text=_("The IP address where spamd can be reached") - ) - - spamd_port = forms.IntegerField( - label=_("Spamd port"), - initial=783, - help_text=_("The TCP port spamd is listening on") - ) - - domain_level_learning = YesNoField( - label=_("Enable per-domain manual learning"), - initial="no", - help_text=_( - "Allow domain administrators to train Spamassassin " - "(within dedicated per-domain databases)" - ) - ) - - user_level_learning = YesNoField( - label=_("Enable per-user manual learning"), - initial="no", - help_text=_( - "Allow simple users to personally train Spamassassin " - "(within a dedicated database)" - ) - ) - - visibility_rules = { - "am_pdp_host": "am_pdp_mode=inet", - "am_pdp_port": "am_pdp_mode=inet", - "am_pdp_socket": "am_pdp_mode=unix", - - "sa_is_local": "manual_learning=yes", - "default_user": "manual_learning=yes", - "spamd_address": "sa_is_local=no", - "spamd_port": "sa_is_local=no", - "domain_level_learning": "manual_learning=yes", - "user_level_learning": "manual_learning=yes" - } - - -class UserSettings(UserParametersForm): - - """Per-user settings.""" - - app = "amavis" - - dsep = SeparatorField(label=_("Display")) - - messages_per_page = forms.IntegerField( - initial=40, - label=_("Number of displayed emails per page"), - help_text=_("Set the maximum number of messages displayed in a page") - ) diff --git a/modoboa/extensions/amavis/dbrouter.py b/modoboa/extensions/amavis/dbrouter.py deleted file mode 100644 index af582ef80..000000000 --- a/modoboa/extensions/amavis/dbrouter.py +++ /dev/null @@ -1,30 +0,0 @@ -class AmavisRouter(object): - """A router to control all database operations on models in - the amavis application""" - - def db_for_read(self, model, **hints): - "Point all operations on amavis models to 'amavis'" - if model._meta.app_label == 'amavis': - return 'amavis' - return None - - def db_for_write(self, model, **hints): - "Point all operations on amavis models to 'amavis'" - if model._meta.app_label == 'amavis': - return 'amavis' - return None - - def allow_relation(self, obj1, obj2, **hints): - "Allow any relation if a model in amavis is involved" - if obj1._meta.app_label == 'amavis' \ - or obj2._meta.app_label == 'amavis': - return True - return None - - def allow_syncdb(self, db, model): - "Make sure the amavis app only appears on the 'amavis' db" - if db == 'amavis': - return model._meta.app_label == 'amavis' - elif model._meta.app_label == 'amavis': - return False - return None diff --git a/modoboa/extensions/amavis/fixtures/demo.json b/modoboa/extensions/amavis/fixtures/demo.json deleted file mode 100644 index 5075ab854..000000000 --- a/modoboa/extensions/amavis/fixtures/demo.json +++ /dev/null @@ -1 +0,0 @@ -[{"pk": 1, "model": "amavis.users", "fields": {"priority": 7, "policy": 1, "email": "@demo.local", "fullname": "demo.local"}}] \ No newline at end of file diff --git a/modoboa/extensions/amavis/forms.py b/modoboa/extensions/amavis/forms.py deleted file mode 100644 index 7af8c2b52..000000000 --- a/modoboa/extensions/amavis/forms.py +++ /dev/null @@ -1,97 +0,0 @@ -# coding: utf-8 -""" -Amavis forms. -""" - -from django import forms -from django.utils.translation import ugettext as _ - -from modoboa.lib import parameters -from modoboa.lib.form_utils import InlineRadioSelect -from .models import Policy, Users - - -class DomainPolicyForm(forms.ModelForm): - - spam_subject_tag2_act = forms.BooleanField() - - class Meta: - model = Policy - fields = ('bypass_virus_checks', 'bypass_spam_checks', - 'spam_tag2_level', 'spam_subject_tag2', - 'spam_kill_level', 'bypass_banned_checks') - widgets = { - 'bypass_virus_checks': InlineRadioSelect(), - 'bypass_spam_checks': InlineRadioSelect(), - 'spam_tag2_level': forms.TextInput( - attrs={'class': 'form-control'}), - 'spam_kill_level': forms.TextInput( - attrs={'class': 'form-control'}), - 'spam_subject_tag2': forms.TextInput( - attrs={'class': 'form-control'}), - 'bypass_banned_checks': InlineRadioSelect(), - } - - def __init__(self, *args, **kwargs): - if "instance" in kwargs: - self.domain = kwargs["instance"] - try: - policy = Users.objects.get( - email="@%s" % self.domain.name).policy - kwargs["instance"] = policy - except (Users.DoesNotExist, Policy.DoesNotExist): - del kwargs["instance"] - super(DomainPolicyForm, self).__init__(*args, **kwargs) - for field in self.fields.keys(): - self.fields[field].required = False - - def save(self, user, commit=True): - policy = super(DomainPolicyForm, self).save(commit=False) - for field in ['bypass_spam_checks', 'bypass_virus_checks', - 'bypass_banned_checks']: - if getattr(policy, field) == '': - setattr(policy, field, None) - - if self.cleaned_data['spam_subject_tag2_act']: - policy.spam_subject_tag2 = None - - if commit: - policy.save() - try: - u = Users.objects.get(fullname=policy.policy_name) - except Users.DoesNotExist: - u = Users.objects.get(email="@%s" % self.domain.name) - u.policy = policy - policy.save() - return policy - - -class LearningRecipientForm(forms.Form): - - """A form to select the recipient of a learning request.""" - - recipient = forms.ChoiceField( - label=None, choices=[] - ) - ltype = forms.ChoiceField( - label="", choices=[("spam", "spam"), ("ham", "ham")], - widget=forms.widgets.HiddenInput - ) - selection = forms.CharField( - label="", widget=forms.widgets.HiddenInput) - - def __init__(self, user, *args, **kwargs): - """Constructor.""" - super(LearningRecipientForm, self).__init__(*args, **kwargs) - choices = [] - if user.group == "SuperAdmins": - choices.append(("global", _("Global database"))) - domain_level_learning = parameters.get_admin( - "DOMAIN_LEVEL_LEARNING") == "yes" - user_level_learning = parameters.get_admin( - "USER_LEVEL_LEARNING") == "yes" - if domain_level_learning: - choices.append(("domain", _("Domain's database"))) - if user_level_learning: - choices.append(("user", _("User's database"))) - self.fields["recipient"].choices = choices diff --git a/modoboa/extensions/amavis/general_callbacks.py b/modoboa/extensions/amavis/general_callbacks.py deleted file mode 100644 index 0859f74db..000000000 --- a/modoboa/extensions/amavis/general_callbacks.py +++ /dev/null @@ -1,213 +0,0 @@ -"""General event callbacks.""" - -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _ -from django.template import Template, Context - -from modoboa.lib import events, parameters -from modoboa.extensions.admin.models import DomainAlias, Alias -from modoboa.extensions.amavis.lib import ( - create_user_and_policy, update_user_and_policy, delete_user_and_policy, - create_user_and_use_policy, delete_user -) -from .lib import manual_learning_enabled -from .models import Policy, Users - - -def create_relay_domains_records(): - """Create records for existing relay domains.""" - from modoboa.extensions.postfix_relay_domains.models import RelayDomain - - for rdom in RelayDomain.objects.all(): - policy = create_user_and_policy("@{0}".format(rdom.name)) - for rdomalias in rdom.relaydomainalias_set.all(): - rdomalias_pattern = "@{0}".format(rdomalias.name) - create_user_and_use_policy(rdomalias_pattern, policy) - - -@events.observe("InitialDataLoaded") -def load_relay_domains_dependant_data(name): - """Load relay domains dependant data.""" - if name != "postfix_relay_domains": - return - create_relay_domains_records() - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target == "top_menu": - return [ - {"name": "quarantine", - "label": _("Quarantine"), - "url": reverse('amavis:index')} - ] - return [] - - -@events.observe("DomainCreated") -def on_domain_created(user, domain): - create_user_and_policy("@{0}".format(domain.name)) - - -@events.observe("DomainModified") -def on_domain_modified(domain): - update_user_and_policy( - "@{0}".format(domain.oldname), - "@{0}".format(domain.name) - ) - - -@events.observe("DomainDeleted") -def on_domain_deleted(domain): - delete_user_and_policy("@{0}".format(domain.name)) - - -@events.observe("DomainAliasCreated") -def on_domain_alias_created(user, domainalias): - create_user_and_use_policy( - "@{0}".format(domainalias.name), - "@{0}".format(domainalias.target.name) - ) - - -@events.observe("DomainAliasDeleted") -def on_domain_alias_deleted(domainaliases): - if isinstance(domainaliases, DomainAlias): - domainaliases = [domainaliases] - for domainalias in domainaliases: - delete_user("@{0}".format(domainalias.name)) - - -@events.observe("MailboxModified") -def on_mailbox_modified(mailbox): - """Update amavis records if address has changed.""" - if parameters.get_admin("MANUAL_LEARNING") == "no" or \ - not hasattr(mailbox, "old_full_address") or \ - mailbox.full_address == mailbox.old_full_address: - return - user = Users.objects.select_related().get(email=mailbox.old_full_address) - full_address = mailbox.full_address - user.email = full_address - user.policy.policy_name = full_address[:32] - user.policy.sa_username = full_address - user.policy.save() - user.save() - - -@events.observe("MailboxDeleted") -def on_mailbox_deleted(mailbox): - """Clean amavis database when a mailbox is removed.""" - if parameters.get_admin("MANUAL_LEARNING") == "no": - return - delete_user_and_policy("@{0}".format(mailbox.full_address)) - - -@events.observe("MailboxAliasCreated") -def on_mailboxalias_created(user, alias): - """Create amavis record for the new alias. - - FIXME: how to deal with distibution lists ? - """ - if not manual_learning_enabled(user) or alias.type != "alias": - return - try: - policy = Policy.objects.get( - policy_name=alias.mboxes.all()[0].full_address - ) - except Policy.DoesNotExist: - return - else: - email = alias.full_address - Users.objects.create( - email=email, policy=policy, fullname=email, priority=7 - ) - - -@events.observe("MailboxAliasDeleted") -def on_mailboxalias_deleted(aliases): - """Clean amavis database when an alias is removed.""" - if parameters.get_admin("MANUAL_LEARNING") == "no": - return - if isinstance(aliases, Alias): - aliases = [aliases] - aliases = [alias.full_address for alias in aliases] - Users.objects.filter(email__in=aliases).delete() - - -@events.observe("GetStaticContent") -def extra_static_content(caller, st_type, user): - if user.group == "SimpleUsers" or st_type != "js": - return [] - - if caller == 'domains': - tpl = Template(""" -""") - - return [tpl.render(Context({}))] - return [] - - -@events.observe("TopNotifications") -def check_for_pending_requests(request, include_all): - """ - Check if release requests are pending. - """ - from .sql_connector import get_connector - - if parameters.get_admin("USER_CAN_RELEASE") == "yes" \ - or request.user.group == "SimpleUsers": - return [] - - nbrequests = get_connector(user=request.user).get_pending_requests() - if not nbrequests: - return [{"id": "nbrequests", "counter": 0}] if include_all \ - else [] - - url = reverse("amavis:index") - url += "#listing/?viewrequests=1" - return [{ - "id": "nbrequests", "url": url, "text": _("Pending requests"), - "counter": nbrequests, "level": "danger" - }] - - -def send_amavis_form(): - """ - """ - from .forms import DomainPolicyForm - return [{ - 'id': 'amavis', 'title': _("Content filter"), 'cls': DomainPolicyForm, - 'formtpl': 'amavis/domain_content_filter.html' - }] - - -@events.observe("ExtraDomainForm") -def extra_domain_form(user, domain): - if not user.has_perm("admin.view_domains"): - return [] - return send_amavis_form() - - -@events.observe('ExtraRelayDomainForm') -def extra_relaydomain_form(user, rdomain): - if not user.has_perm("postfix_relay_domains.add_relaydomain"): - return [] - return send_amavis_form() - - -@events.observe("FillDomainInstances") -def fill_domain_instances(user, domain, instances): - if not user.has_perm("admin.view_domains"): - return - instances["amavis"] = domain - - -@events.observe("FillRelayDomainInstances") -def fill_relaydomain_instances(user, rdomain, instances): - if not user.has_perm("postfix_relay_domains.add_relaydomain"): - return - instances["amavis"] = rdomain diff --git a/modoboa/extensions/amavis/lib.py b/modoboa/extensions/amavis/lib.py deleted file mode 100644 index a1ab982e7..000000000 --- a/modoboa/extensions/amavis/lib.py +++ /dev/null @@ -1,345 +0,0 @@ -# coding: utf-8 - -from functools import wraps -import re -import socket -import string -import struct - -from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse - -from modoboa.extensions.admin.models import Mailbox, Alias -from modoboa.lib import parameters -from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.exceptions import InternalError -from modoboa.lib.sysutils import exec_cmd -from modoboa.lib.web_utils import NavigationParameters -from .models import Users, Policy - - -def selfservice(ssfunc=None): - """Decorator used to expose views to the 'self-service' feature - - The 'self-service' feature allows users to act on quarantined - messages without beeing authenticated. - - This decorator only acts as a 'router'. - - :param ssfunc: the function to call if the 'self-service' - pre-requisites are satisfied - """ - def decorator(f): - @wraps(f) - def wrapped_f(request, *args, **kwargs): - if request.user.is_authenticated(): - return f(request, *args, **kwargs) - if parameters.get_admin("SELF_SERVICE") == "no": - from django.contrib.auth.views import redirect_to_login - return redirect_to_login( - reverse("amavis:index") - ) - return ssfunc(request, *args, **kwargs) - return wrapped_f - return decorator - - -class AMrelease(object): - def __init__(self): - mode = parameters.get_admin("AM_PDP_MODE") - try: - if mode == "inet": - host = parameters.get_admin('AM_PDP_HOST') - port = parameters.get_admin('AM_PDP_PORT') - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect((host, int(port))) - else: - path = parameters.get_admin('AM_PDP_SOCKET') - self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.sock.connect(path) - except socket.error, err: - raise InternalError( - _("Connection to amavis failed: %s" % str(err)) - ) - - def decode(self, answer): - def repl(match): - return struct.pack("B", string.atoi(match.group(0)[1:], 16)) - - return re.sub(r"%([0-9a-fA-F]{2})", repl, answer) - - def __del__(self): - self.sock.close() - - def sendreq(self, mailid, secretid, recipient, *others): - self.sock.send("""request=release -mail_id=%s -secret_id=%s -quar_type=Q -recipient=%s - -""" % (mailid, secretid, recipient)) - answer = self.sock.recv(1024) - answer = self.decode(answer) - if re.search(r"250 [\d\.]+ Ok", answer): - return True - return False - - -class SpamassassinClient(object): - - """A stupid spamassassin client.""" - - def __init__(self, user, recipient_db): - """Constructor.""" - self._sa_is_local = parameters.get_admin("SA_IS_LOCAL") - self._default_username = parameters.get_admin("DEFAULT_USER") - self._recipient_db = recipient_db - self._setup_cache = {} - self._username_cache = [] - if user.group == "SimpleUsers": - user_level_learning = parameters.get_admin("USER_LEVEL_LEARNING") - if user_level_learning == "yes": - self._username = user.email - else: - self._username = None - self.error = None - if self._sa_is_local == "yes": - self._learn_cmd = "sa-learn --{0} --no-sync -u {1}" - self._learn_cmd_kwargs = {} - self._expected_exit_codes = [0] - else: - self._learn_cmd = "spamc -d {0} -p {1}".format( - parameters.get_admin("SPAMD_ADDRESS"), - parameters.get_admin("SPAMD_PORT") - ) - self._learn_cmd += " -L {0} -u {1}" - self._learn_cmd_kwargs = {} - self._expected_exit_codes = [5, 6] - - def _get_mailbox_from_rcpt(self, rcpt): - """Retrieve a mailbox from a recipient address.""" - local_part, domname = split_mailbox(rcpt) - try: - mailbox = Mailbox.objects.select_related("domain").get( - address=local_part, domain__name=domname) - except Mailbox.DoesNotExist: - try: - alias = Alias.objects.select_related("domain").get( - address=local_part, domain__name=domname) - except Alias.DoesNotExist: - raise InternalError(_("No recipient found")) - if alias.type != "alias": - return None - mailbox = alias.mboxes.all()[0] - return mailbox - - def _learn(self, rcpt, msg, mtype): - """Internal method to call the learning command.""" - if self._username is None: - if self._recipient_db == "global": - username = self._default_username - else: - mbox = self._get_mailbox_from_rcpt(rcpt) - if mbox is None: - username = self._default_username - if self._recipient_db == "domain": - username = mbox.domain.name - if username not in self._setup_cache and \ - setup_manual_learning_for_domain(mbox.domain): - self._setup_cache[username] = True - else: - username = mbox.full_address - if username not in self._setup_cache and \ - setup_manual_learning_for_mbox(mbox): - self._setup_cache[username] = True - else: - username = self._username - if username not in self._username_cache: - self._username_cache.append(username) - cmd = self._learn_cmd.format(mtype, username) - code, output = exec_cmd(cmd, pinput=msg, **self._learn_cmd_kwargs) - if code in self._expected_exit_codes: - return True - self.error = output - return False - - def learn_spam(self, rcpt, msg): - """Learn new spam.""" - return self._learn(rcpt, msg, "spam") - - def learn_ham(self, rcpt, msg): - """Learn new ham.""" - return self._learn(rcpt, msg, "ham") - - def done(self): - """Call this method at the end of the processing.""" - if self._sa_is_local: - for username in self._username_cache: - exec_cmd("sa-learn -u {0} --sync".format(username), - **self._learn_cmd_kwargs) - - -class QuarantineNavigationParameters(NavigationParameters): - """ - Specific NavigationParameters subclass for the quarantine. - """ - def __init__(self, request): - super(QuarantineNavigationParameters, self).__init__( - request, 'quarantine_navparams' - ) - self.parameters += [ - ('pattern', '', False), - ('criteria', 'from_addr', False), - ('msgtype', None, False), - ('viewrequests', None, False) - ] - - def _store_page(self): - """Specific method to store the current page.""" - if self.request.GET.get("reset_page", None) or "page" not in self: - self["page"] = 1 - else: - page = self.request.GET.get("page", None) - if page is not None: - self["page"] = int(page) - - def back_to_listing(self): - """Return the current listing URL. - - Looks into the user's session and the current request to build - the URL. - - :return: a string - """ - url = "listing" - params = [] - navparams = self.request.session[self.sessionkey] - if "page" in navparams: - params += ["page=%s" % navparams["page"]] - if "order" in navparams: - params += ["sort_order=%s" % navparams["order"]] - params += ["%s=%s" % (p[0], navparams[p[0]]) - for p in self.parameters if p[0] in navparams] - if params: - url += "?%s" % ("&".join(params)) - return url - - -def create_user_and_policy(name, priority=7): - """Create records. - - Create two records (a user and a policy) using :keyword:`name` as - an identifier. - - :param str name: name - :return: the new ``Policy`` object - """ - if Users.objects.filter(email=name).exists(): - return Policy.objects.get(policy_name=name[:32]) - policy = Policy.objects.create(policy_name=name[:32]) - Users.objects.create( - email=name, fullname=name, priority=priority, policy=policy - ) - return policy - - -def create_user_and_use_policy(name, policy, priority=7): - """Create a *users* record and use an existing policy. - - :param str name: user record name - :param str policy: string or Policy instance - """ - if isinstance(policy, basestring): - policy = Policy.objects.get(policy_name=policy[:32]) - Users.objects.get_or_create( - email=name, fullname=name, priority=priority, policy=policy - ) - - -def update_user_and_policy(oldname, newname): - """Update records. - - :param str oldname: old name - :param str newname: new name - """ - if oldname == newname: - return - u = Users.objects.get(email=oldname) - u.email = newname - u.fullname = newname - u.policy.policy_name = newname[:32] - u.policy.save() - u.save() - - -def delete_user_and_policy(name): - """Delete records. - - :param str name: identifier - """ - try: - u = Users.objects.get(email=name) - except Users.DoesNotExist: - return - u.policy.delete() - u.delete() - - -def delete_user(name): - """Delete a *users* record. - - :param str name: user record name - """ - try: - Users.objects.get(email=name).delete() - except Users.DoesNotExist: - pass - - -def manual_learning_enabled(user): - """Check if manual learning is enabled or not. - - Also check for :kw:`user` if necessary. - - :return: True if learning is enabled, False otherwise. - """ - manual_learning = parameters.get_admin("MANUAL_LEARNING") == "yes" - if manual_learning and user.group != 'SuperAdmins': - domain_level_learning = parameters.get_admin( - "DOMAIN_LEVEL_LEARNING") == "yes" - user_level_learning = parameters.get_admin( - "USER_LEVEL_LEARNING") == "yes" - if user.has_perm("admin.view_domains"): - manual_learning = domain_level_learning or user_level_learning - else: - manual_learning = user_level_learning - return manual_learning - - -def setup_manual_learning_for_domain(domain): - """Setup manual learning if necessary. - - :return: True if learning has been setup, False otherwise - """ - if Policy.objects.filter(sa_username=domain.name).exists(): - return False - policy = Policy.objects.get(policy_name=domain.name[:32]) - policy.sa_username = domain.name - policy.save() - return True - - -def setup_manual_learning_for_mbox(mbox): - """Setup manual learning if necessary. - - :return: True if learning has been setup, False otherwise - """ - result = False - pname = mbox.full_address[:32] - if not Policy.objects.filter(policy_name=pname).exists(): - policy = create_user_and_policy(pname) - for alias in mbox.alias_addresses: - create_user_and_use_policy(alias, policy) - result = True - return result diff --git a/modoboa/extensions/amavis/management/__init__.py b/modoboa/extensions/amavis/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/amavis/management/commands/__init__.py b/modoboa/extensions/amavis/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/amavis/management/commands/amnotify.py b/modoboa/extensions/amavis/management/commands/amnotify.py deleted file mode 100644 index 577a81308..000000000 --- a/modoboa/extensions/amavis/management/commands/amnotify.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -from optparse import make_option - -from django.core.management.base import BaseCommand, CommandError -from django.template.loader import render_to_string -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _ - -from modoboa.core.management.commands import CloseConnectionMixin -from modoboa.core.models import User -from modoboa.lib import parameters -from modoboa.lib.email_utils import sendmail_simple -from modoboa.extensions.admin.models import Domain - -from modoboa.extensions.amavis.models import ( - Msgrcpt -) -from ...modo_extension import Amavis -from ...sql_connector import get_connector - - -class Command(BaseCommand, CloseConnectionMixin): - - """A management command to notify admins about pending release requests.""" - - help = 'Amavis notification tool' - - sender = None - baseurl = None - listingurl = None - - option_list = BaseCommand.option_list + ( - make_option("--baseurl", type="string", default=None, - help="The scheme and hostname used to access Modoboa"), - make_option("--smtp_host", type="string", default="localhost", - help="The address of the SMTP server used to send notifications"), - make_option("--smtp_port", type="int", default=25, - help="The listening port of the SMTP server used to send notifications"), - make_option("--verbose", action="store_true", - help="Activate verbose mode") - ) - - def handle(self, *args, **options): - if options["baseurl"] is None: - raise CommandError("You must provide the --baseurl option") - Amavis().load() - self.options = options - self.notify_admins_pending_requests() - - def send_pr_notification(self, rcpt, reqs): - if self.options["verbose"]: - print "Sending notification to %s" % rcpt - total = reqs.count() - reqs = reqs.all()[:10] - content = render_to_string( - "amavis/notifications/pending_requests.html", dict( - total=total, requests=reqs, - baseurl=self.baseurl, listingurl=self.listingurl - ) - ) - status, msg = sendmail_simple( - self.sender, rcpt, - subject=_("[modoboa] Pending release requests"), - content=content, - server=self.options["smtp_host"], - port=self.options["smtp_port"] - ) - if not status: - print msg - - def notify_admins_pending_requests(self): - self.sender = parameters.get_admin("NOTIFICATIONS_SENDER", - app="amavis") - self.baseurl = self.options["baseurl"].strip("/") - self.listingurl = self.baseurl \ - + reverse("amavis:_mail_list") \ - + "?viewrequests=1" - - for da in User.objects.filter(groups__name="DomainAdmins"): - if not da.mailbox_set.count(): - continue - rcpt = da.mailbox_set.all()[0].full_address - reqs = get_connector().get_domains_pending_requests( - Domain.objects.get_for_admin(da) - ) - if reqs.count(): - self.send_pr_notification(rcpt, reqs) - - reqs = Msgrcpt.objects.filter(rs='p') - if not reqs.count(): - if self.options["verbose"]: - print "No release request currently pending" - return - for su in User.objects.filter(is_superuser=True): - if not su.mailbox_set.count(): - continue - rcpt = su.mailbox_set.all()[0].full_address - self.send_pr_notification(rcpt, reqs) diff --git a/modoboa/extensions/amavis/management/commands/qcleanup.py b/modoboa/extensions/amavis/management/commands/qcleanup.py deleted file mode 100644 index eaf1ed283..000000000 --- a/modoboa/extensions/amavis/management/commands/qcleanup.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -import time -from optparse import make_option - -from django.core.management.base import BaseCommand - -from modoboa.core.management.commands import CloseConnectionMixin -from modoboa.extensions.amavis.models import ( - Msgrcpt, Msgs, Maddr -) -from modoboa.lib import parameters -from ...modo_extension import Amavis - - -class Command(BaseCommand, CloseConnectionMixin): - args = '' - help = 'Amavis quarantine cleanup' - - option_list = BaseCommand.option_list + ( - make_option('--debug', - action='store_true', - default=False, - help='Activate debug output'), - make_option('--verbose', - action='store_true', - default=False, - help='Display informational messages') - ) - - def __vprint(self, msg): - if not self.verbose: - return - print msg - - def handle(self, *args, **options): - Amavis().load() - if options["debug"]: - import logging - l = logging.getLogger("django.db.backends") - l.setLevel(logging.DEBUG) - l.addHandler(logging.StreamHandler()) - self.verbose = options["verbose"] - - max_messages_age = int(parameters.get_admin("MAX_MESSAGES_AGE", - app="amavis")) - - flags = ['D'] - if parameters.get_admin("RELEASED_MSGS_CLEANUP", - app="amavis") == "yes": - flags += ['R'] - - self.__vprint("Deleting marked messages...") - ids = Msgrcpt.objects.filter(rs__in=flags).values("mail_id").distinct() - for msg in Msgs.objects.filter(mail_id__in=ids): - if not msg.msgrcpt_set.exclude(rs__in=flags).count(): - msg.delete() - - self.__vprint( - "Deleting messages older than %d days..." % max_messages_age) - limit = int(time.time()) - (max_messages_age * 24 * 3600) - Msgs.objects.filter(time_num__lt=limit).delete() - - self.__vprint("Deleting unreferenced e-mail addresses...") - for maddr in Maddr.objects.all(): - if not maddr.msgs_set.count() and not maddr.msgrcpt_set.count(): - maddr.delete() - - self.__vprint("Done.") diff --git a/modoboa/extensions/amavis/migrations/0001_initial.py b/modoboa/extensions/amavis/migrations/0001_initial.py deleted file mode 100644 index dea55f387..000000000 --- a/modoboa/extensions/amavis/migrations/0001_initial.py +++ /dev/null @@ -1,187 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Maddr', - fields=[ - ('partition_tag', models.IntegerField(unique=True, null=True, blank=True)), - ('id', models.BigIntegerField(serialize=False, primary_key=True)), - ('email', models.CharField(unique=True, max_length=255)), - ('domain', models.CharField(max_length=765)), - ], - options={ - 'db_table': 'maddr', - 'managed': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Mailaddr', - fields=[ - ('id', models.IntegerField(serialize=False, primary_key=True)), - ('priority', models.IntegerField()), - ('email', models.CharField(unique=True, max_length=255)), - ], - options={ - 'db_table': 'mailaddr', - 'managed': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Msgs', - fields=[ - ('partition_tag', models.IntegerField(null=True, blank=True)), - ('mail_id', models.CharField(max_length=12, serialize=False, primary_key=True)), - ('secret_id', models.CharField(max_length=12, blank=True)), - ('am_id', models.CharField(max_length=60)), - ('time_num', models.IntegerField()), - ('time_iso', models.CharField(max_length=48)), - ('policy', models.CharField(max_length=765, blank=True)), - ('client_addr', models.CharField(max_length=765, blank=True)), - ('size', models.IntegerField()), - ('originating', models.CharField(max_length=3)), - ('content', models.CharField(max_length=1, blank=True)), - ('quar_type', models.CharField(max_length=1, blank=True)), - ('quar_loc', models.CharField(max_length=255, blank=True)), - ('dsn_sent', models.CharField(max_length=3, blank=True)), - ('spam_level', models.FloatField(null=True, blank=True)), - ('message_id', models.CharField(max_length=765, blank=True)), - ('from_addr', models.CharField(max_length=765, blank=True)), - ('subject', models.CharField(max_length=765, blank=True)), - ('host', models.CharField(max_length=765)), - ], - options={ - 'db_table': 'msgs', - 'managed': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Msgrcpt', - fields=[ - ('partition_tag', models.IntegerField(null=True, blank=True)), - ('mail', models.ForeignKey(primary_key=True, serialize=False, to='amavis.Msgs')), - ('rseqnum', models.IntegerField(default=0)), - ('is_local', models.CharField(max_length=3)), - ('content', models.CharField(max_length=3)), - ('ds', models.CharField(max_length=3)), - ('rs', models.CharField(max_length=3)), - ('bl', models.CharField(max_length=3, blank=True)), - ('wl', models.CharField(max_length=3, blank=True)), - ('bspam_level', models.FloatField(null=True, blank=True)), - ('smtp_resp', models.CharField(max_length=765, blank=True)), - ], - options={ - 'db_table': 'msgrcpt', - 'managed': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Policy', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('policy_name', models.CharField(max_length=32, blank=True)), - ('virus_lover', models.CharField(max_length=3, null=True, blank=True)), - ('spam_lover', models.CharField(max_length=3, null=True, blank=True)), - ('unchecked_lover', models.CharField(max_length=3, null=True, blank=True)), - ('banned_files_lover', models.CharField(max_length=3, null=True, blank=True)), - ('bad_header_lover', models.CharField(max_length=3, null=True, blank=True)), - ('bypass_virus_checks', models.CharField(default=b'', choices=[(b'N', 'yes'), (b'Y', 'no'), (b'', 'default')], max_length=3, help_text="Bypass virus checks or not. Choose 'default' to use global settings.", null=True, verbose_name='Virus filter')), - ('bypass_spam_checks', models.CharField(default=b'', choices=[(b'N', 'yes'), (b'Y', 'no'), (b'', 'default')], max_length=3, help_text="Bypass spam checks or not. Choose 'default' to use global settings.", null=True, verbose_name='Spam filter')), - ('bypass_banned_checks', models.CharField(default=b'', choices=[(b'N', 'yes'), (b'Y', 'no'), (b'', 'default')], max_length=3, help_text="Bypass banned checks or not. Choose 'default' to use global settings.", null=True, verbose_name='Banned filter')), - ('bypass_header_checks', models.CharField(max_length=3, null=True, blank=True)), - ('virus_quarantine_to', models.CharField(max_length=192, null=True, blank=True)), - ('spam_quarantine_to', models.CharField(max_length=192, null=True, blank=True)), - ('banned_quarantine_to', models.CharField(max_length=192, null=True, blank=True)), - ('unchecked_quarantine_to', models.CharField(max_length=192, null=True, blank=True)), - ('bad_header_quarantine_to', models.CharField(max_length=192, null=True, blank=True)), - ('clean_quarantine_to', models.CharField(max_length=192, null=True, blank=True)), - ('archive_quarantine_to', models.CharField(max_length=192, null=True, blank=True)), - ('spam_tag_level', models.FloatField(null=True, blank=True)), - ('spam_tag2_level', models.FloatField(null=True, blank=True)), - ('spam_tag3_level', models.FloatField(null=True, blank=True)), - ('spam_kill_level', models.FloatField(null=True, blank=True)), - ('spam_dsn_cutoff_level', models.FloatField(null=True, blank=True)), - ('spam_quarantine_cutoff_level', models.FloatField(null=True, blank=True)), - ('addr_extension_virus', models.CharField(max_length=192, null=True, blank=True)), - ('addr_extension_spam', models.CharField(max_length=192, null=True, blank=True)), - ('addr_extension_banned', models.CharField(max_length=192, null=True, blank=True)), - ('addr_extension_bad_header', models.CharField(max_length=192, null=True, blank=True)), - ('warnvirusrecip', models.CharField(max_length=3, null=True, blank=True)), - ('warnbannedrecip', models.CharField(max_length=3, null=True, blank=True)), - ('warnbadhrecip', models.CharField(max_length=3, null=True, blank=True)), - ('newvirus_admin', models.CharField(max_length=192, null=True, blank=True)), - ('virus_admin', models.CharField(max_length=192, null=True, blank=True)), - ('banned_admin', models.CharField(max_length=192, null=True, blank=True)), - ('bad_header_admin', models.CharField(max_length=192, null=True, blank=True)), - ('spam_admin', models.CharField(max_length=192, null=True, blank=True)), - ('spam_subject_tag', models.CharField(max_length=192, null=True, blank=True)), - ('spam_subject_tag2', models.CharField(default=None, max_length=192, blank=True, help_text="Modify spam subject using the specified text. Choose 'default' to use global settings.", null=True, verbose_name='Spam marker')), - ('spam_subject_tag3', models.CharField(max_length=192, null=True, blank=True)), - ('message_size_limit', models.IntegerField(null=True, blank=True)), - ('banned_rulenames', models.CharField(max_length=192, null=True, blank=True)), - ('disclaimer_options', models.CharField(max_length=192, null=True, blank=True)), - ('forward_method', models.CharField(max_length=192, null=True, blank=True)), - ('sa_userconf', models.CharField(max_length=192, null=True, blank=True)), - ('sa_username', models.CharField(max_length=192, null=True, blank=True)), - ], - options={ - 'db_table': 'policy', - 'managed': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Quarantine', - fields=[ - ('partition_tag', models.IntegerField(null=True, blank=True)), - ('mail', models.ForeignKey(primary_key=True, serialize=False, to='amavis.Msgs')), - ('chunk_ind', models.IntegerField()), - ('mail_text', models.TextField()), - ], - options={ - 'ordering': ['-mail__time_num'], - 'db_table': 'quarantine', - 'managed': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Users', - fields=[ - ('id', models.AutoField(serialize=False, primary_key=True)), - ('priority', models.IntegerField()), - ('email', models.CharField(unique=True, max_length=255)), - ('fullname', models.CharField(max_length=765, blank=True)), - ], - options={ - 'db_table': 'users', - 'managed': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Wblist', - fields=[ - ('rid', models.IntegerField(serialize=False, primary_key=True)), - ('sid', models.IntegerField(primary_key=True)), - ('wb', models.CharField(max_length=30)), - ], - options={ - 'db_table': 'wblist', - 'managed': False, - }, - bases=(models.Model,), - ), - ] diff --git a/modoboa/extensions/amavis/migrations/__init__.py b/modoboa/extensions/amavis/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/amavis/models.py b/modoboa/extensions/amavis/models.py deleted file mode 100644 index 3a9876484..000000000 --- a/modoboa/extensions/amavis/models.py +++ /dev/null @@ -1,236 +0,0 @@ -# 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. -# -# Original Amavis version : 2.6.2 - -from django.db import models -from django.utils.translation import ugettext_lazy - - -class Maddr(models.Model): - partition_tag = models.IntegerField(unique=True, null=True, blank=True) - id = models.BigIntegerField(primary_key=True) - email = models.CharField(unique=True, max_length=255) - domain = models.CharField(max_length=765) - - class Meta: - db_table = u'maddr' - managed = False - - -class Mailaddr(models.Model): - id = models.IntegerField(primary_key=True) - priority = models.IntegerField() - email = models.CharField(unique=True, max_length=255) - - class Meta: - db_table = u'mailaddr' - managed = False - - -class Msgs(models.Model): - partition_tag = models.IntegerField(null=True, blank=True) - mail_id = models.CharField(max_length=12, primary_key=True) - secret_id = models.CharField(max_length=12, blank=True) - am_id = models.CharField(max_length=60) - time_num = models.IntegerField() - time_iso = models.CharField(max_length=48) - sid = models.ForeignKey(Maddr, db_column='sid') - policy = models.CharField(max_length=765, blank=True) - client_addr = models.CharField(max_length=765, blank=True) - size = models.IntegerField() - originating = models.CharField(max_length=3) - content = models.CharField(max_length=1, blank=True) - quar_type = models.CharField(max_length=1, blank=True) - quar_loc = models.CharField(max_length=255, blank=True) - dsn_sent = models.CharField(max_length=3, blank=True) - spam_level = models.FloatField(null=True, blank=True) - message_id = models.CharField(max_length=765, blank=True) - from_addr = models.CharField(max_length=765, blank=True) - subject = models.CharField(max_length=765, blank=True) - host = models.CharField(max_length=765) - - class Meta: - db_table = u'msgs' - managed = False - unique_together = ('partition_tag', 'mail_id') - - -class Msgrcpt(models.Model): - partition_tag = models.IntegerField(null=True, blank=True) - mail = models.ForeignKey(Msgs, primary_key=True) - rid = models.ForeignKey(Maddr, db_column='rid') - rseqnum = models.IntegerField(default=0) - is_local = models.CharField(max_length=3) - content = models.CharField(max_length=3) - ds = models.CharField(max_length=3) - rs = models.CharField(max_length=3) - bl = models.CharField(max_length=3, blank=True) - wl = models.CharField(max_length=3, blank=True) - bspam_level = models.FloatField(null=True, blank=True) - smtp_resp = models.CharField(max_length=765, blank=True) - - class Meta: - db_table = u'msgrcpt' - managed = False - unique_together = ("partition_tag", "mail", "rseqnum") - - -class Policy(models.Model): - policy_name = models.CharField(max_length=32, blank=True) - virus_lover = models.CharField(max_length=3, blank=True, null=True) - spam_lover = models.CharField(max_length=3, blank=True, null=True) - unchecked_lover = models.CharField(max_length=3, blank=True, null=True) - banned_files_lover = models.CharField(max_length=3, blank=True, null=True) - bad_header_lover = models.CharField(max_length=3, blank=True, null=True) - bypass_virus_checks = models.CharField( - ugettext_lazy("Virus filter"), default='', null=True, - choices=(('N', ugettext_lazy('yes')), - ('Y', ugettext_lazy('no')), - ('', ugettext_lazy('default'))), - max_length=3, - help_text=ugettext_lazy( - "Bypass virus checks or not. Choose 'default' to use global " - "settings." - ) - ) - bypass_spam_checks = models.CharField( - ugettext_lazy("Spam filter"), default='', null=True, - choices=(('N', ugettext_lazy('yes')), - ('Y', ugettext_lazy('no')), - ('', ugettext_lazy('default'))), - max_length=3, - help_text=ugettext_lazy( - "Bypass spam checks or not. Choose 'default' to use global " - "settings." - ) - ) - bypass_banned_checks = models.CharField( - ugettext_lazy("Banned filter"), default='', null=True, - choices=(('N', ugettext_lazy('yes')), - ('Y', ugettext_lazy('no')), - ('', ugettext_lazy('default'))), - max_length=3, - help_text=ugettext_lazy( - "Bypass banned checks or not. Choose 'default' to use global " - "settings." - ) - ) - bypass_header_checks = models.CharField(max_length=3, blank=True, null=True) - virus_quarantine_to = models.CharField( - max_length=192, - blank=True, - null=True) - spam_quarantine_to = models.CharField(max_length=192, blank=True, null=True) - banned_quarantine_to = models.CharField( - max_length=192, - blank=True, - null=True) - unchecked_quarantine_to = models.CharField( - max_length=192, - blank=True, - null=True) - bad_header_quarantine_to = models.CharField( - max_length=192, - blank=True, - null=True) - clean_quarantine_to = models.CharField( - max_length=192, - blank=True, - null=True) - archive_quarantine_to = models.CharField( - max_length=192, - blank=True, - null=True) - spam_tag_level = models.FloatField(null=True, blank=True) - spam_tag2_level = models.FloatField(null=True, blank=True) - spam_tag3_level = models.FloatField(null=True, blank=True) - spam_kill_level = models.FloatField(null=True, blank=True) - spam_dsn_cutoff_level = models.FloatField(null=True, blank=True) - spam_quarantine_cutoff_level = models.FloatField(null=True, blank=True) - addr_extension_virus = models.CharField( - max_length=192, - blank=True, - null=True) - addr_extension_spam = models.CharField( - max_length=192, - blank=True, - null=True) - addr_extension_banned = models.CharField( - max_length=192, - blank=True, - null=True) - addr_extension_bad_header = models.CharField( - max_length=192, - blank=True, - null=True) - warnvirusrecip = models.CharField(max_length=3, blank=True, null=True) - warnbannedrecip = models.CharField(max_length=3, blank=True, null=True) - warnbadhrecip = models.CharField(max_length=3, blank=True, null=True) - newvirus_admin = models.CharField(max_length=192, blank=True, null=True) - virus_admin = models.CharField(max_length=192, blank=True, null=True) - banned_admin = models.CharField(max_length=192, blank=True, null=True) - bad_header_admin = models.CharField(max_length=192, blank=True, null=True) - spam_admin = models.CharField(max_length=192, blank=True, null=True) - spam_subject_tag = models.CharField(max_length=192, blank=True, null=True) - spam_subject_tag2 = models.CharField( - ugettext_lazy("Spam marker"), default=None, - max_length=192, blank=True, null=True, - help_text=ugettext_lazy( - "Modify spam subject using the specified text. Choose 'default' to " - "use global settings." - ) - ) - spam_subject_tag3 = models.CharField(max_length=192, blank=True, null=True) - message_size_limit = models.IntegerField(null=True, blank=True) - banned_rulenames = models.CharField(max_length=192, blank=True, null=True) - disclaimer_options = models.CharField(max_length=192, blank=True, null=True) - forward_method = models.CharField(max_length=192, blank=True, null=True) - sa_userconf = models.CharField(max_length=192, blank=True, null=True) - sa_username = models.CharField(max_length=192, blank=True, null=True) - - class Meta: - db_table = u'policy' - managed = False - - -class Quarantine(models.Model): - partition_tag = models.IntegerField(null=True, blank=True) - mail = models.ForeignKey(Msgs, primary_key=True) - chunk_ind = models.IntegerField() - mail_text = models.BinaryField() - - class Meta: - db_table = u'quarantine' - managed = False - ordering = ["-mail__time_num"] - unique_together = ("partition_tag", "mail", "chunk_ind") - - -class Users(models.Model): - id = models.AutoField(primary_key=True) - priority = models.IntegerField() - policy = models.ForeignKey(Policy) - email = models.CharField(unique=True, max_length=255) - fullname = models.CharField(max_length=765, blank=True) - - class Meta: - db_table = u'users' - managed = False - - -class Wblist(models.Model): - rid = models.IntegerField(primary_key=True) - sid = models.IntegerField(primary_key=True) - wb = models.CharField(max_length=30) - - class Meta: - db_table = u'wblist' - managed = False diff --git a/modoboa/extensions/amavis/modo_extension.py b/modoboa/extensions/amavis/modo_extension.py deleted file mode 100644 index 81b287a44..000000000 --- a/modoboa/extensions/amavis/modo_extension.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding: utf-8 -""" -Amavis management frontend. - -Provides: - -* SQL quarantine management -* Per-domain settings - -""" - -from django.utils.translation import ugettext_lazy - -from modoboa.core.extensions import ModoExtension, exts_pool -from modoboa.extensions.admin.models import Domain -from modoboa.lib import parameters - -from . import general_callbacks -from .lib import create_user_and_policy, create_user_and_use_policy - - -class Amavis(ModoExtension): - - """The Amavis extension.""" - - name = "amavis" - label = "Amavis frontend" - version = "1.0" - description = ugettext_lazy("Simple amavis management frontend") - url = "quarantine" - available_for_topredirection = True - - def load(self): - from .app_settings import ParametersForm, UserSettings - - parameters.register(ParametersForm, "Amavis") - parameters.register(UserSettings, ugettext_lazy("Quarantine")) - - def load_initial_data(self): - """Create records for existing domains and co.""" - for dom in Domain.objects.all(): - policy = create_user_and_policy("@{0}".format(dom.name)) - for domalias in dom.domainalias_set.all(): - domalias_pattern = "@{0}".format(domalias.name) - create_user_and_use_policy(domalias_pattern, policy) - - if not exts_pool.is_extension_installed( - "modoboa.extensions.postfix_relay_domains"): - return - general_callbacks.create_relay_domains_records() - -exts_pool.register_extension(Amavis) diff --git a/modoboa/extensions/amavis/sql_connector.py b/modoboa/extensions/amavis/sql_connector.py deleted file mode 100644 index e21038d59..000000000 --- a/modoboa/extensions/amavis/sql_connector.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -SQL connector module. -""" - -import datetime - -from django.db.models import Q - -from modoboa.extensions.admin.models import Domain -from modoboa.lib.db_utils import db_type - -from .models import Quarantine, Msgrcpt, Maddr - - -class SQLconnector(object): - - """ - This class handles all database operations. - """ - - ORDER_TRANSLATION_TABLE = { - "type": "content", - "score": "bspam_level", - "date": "mail__time_num", - "subject": "mail__subject", - "from": "mail__from_addr", - "to": "rid__email" - } - - QUARANTINE_FIELDS = [ - "content", - "bspam_level", - "rs", - "rid__email", - "mail__from_addr", - "mail__subject", - "mail__mail_id", - "mail__time_num", - ] - - def __init__(self, user=None, navparams=None): - """Constructor.""" - self.user = user - self.navparams = navparams - self.messages = None - - self._messages_count = None - - def _exec(self, query, args): - """Execute a raw SQL query. - - :param string query: query to execute - :param list args: a list of arguments to replace in :kw:`query` - """ - from django.db import connections, transaction - - cursor = connections['amavis'].cursor() - cursor.execute(query, args) - transaction.commit_unless_managed(using='amavis') - - def _apply_msgrcpt_filters(self, flt): - """Apply filters based on user's role. - - """ - if self.user.group == 'SimpleUsers': - rcpts = [self.user.email] - if self.user.mailbox_set.count(): - rcpts += self.user.mailbox_set.all()[0].alias_addresses - return flt & Q(rid__email__in=rcpts) - elif not self.user.is_superuser: - doms = Domain.objects.get_for_admin(self.user) - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - return flt & Q(rid__email__regex=regexp) - return flt - - def _apply_extra_search_filter(self, crit, pattern): - """Apply search filters using additional criterias. - - """ - if crit == "to": - return Q(rid__email__contains=pattern) - return None - - def _apply_extra_select_filters(self, messages): - """Just a hook to apply additional filters to the queryset. - - """ - return messages - - def _get_quarantine_content(self): - """Fetch quarantine content. - - Filters: rs, rid, content - """ - flt = Q(rs__in=[' ', 'V', 'R', 'p', 'S', 'H']) \ - if self.navparams.get('viewrequests', '0') != '1' else Q(rs='p') - flt = self._apply_msgrcpt_filters(flt) - pattern = self.navparams.get("pattern", "") - if pattern: - criteria = self.navparams.get('criteria') - if criteria == "both": - criteria = "from_addr,subject,to" - search_flt = None - for crit in criteria.split(","): - if crit == "from_addr": - nfilter = Q(mail__from_addr__contains=pattern) - elif crit == "subject": - nfilter = Q(mail__subject__contains=pattern) - else: - nfilter = self._apply_extra_search_filter(crit, pattern) - if nfilter is None: - continue - search_flt = nfilter \ - if search_flt is None else search_flt | nfilter - if search_flt: - flt &= search_flt - msgtype = self.navparams.get('msgtype', None) - if msgtype is not None: - flt &= Q(content=msgtype) - - flt &= Q( - mail__in=Quarantine.objects.filter(chunk_ind=1).values("mail_id") - ) - - messages = Msgrcpt.objects.select_related("mail", "rid").filter(flt) - messages = self._apply_extra_select_filters(messages) - return messages - - def messages_count(self): - """Return the total number of messages living in the quarantine. - - We also store the built queryset for a later use. - """ - if self.user is None or self.navparams is None: - return None - if self._messages_count is None: - self.messages = self._get_quarantine_content() - self.messages = self.messages.values(*self.QUARANTINE_FIELDS) - - order = self.navparams.get("order") - if order is not None: - sign = "" - if order[0] == "-": - sign = "-" - order = order[1:] - order = self.ORDER_TRANSLATION_TABLE[order] - self.messages = self.messages.order_by(sign + order) - - self._messages_count = self.messages.count() - - return self._messages_count - - def fetch(self, start=None, stop=None): - """Fetch a range of messages from the internal cache. - - """ - emails = [] - for qm in self.messages[start - 1:stop]: - if qm["rs"] == 'D': - continue - m = { - "from": qm["mail__from_addr"], - "to": qm["rid__email"], - "subject": qm["mail__subject"], - "mailid": qm["mail__mail_id"], - "date": datetime.datetime.fromtimestamp(qm["mail__time_num"]), - "type": qm["content"], - "score": qm["bspam_level"], - "status": qm["rs"] - } - if qm["rs"] in ['', ' ']: - m["class"] = "unseen" - elif qm["rs"] == 'p': - m["class"] = "pending" - emails.append(m) - return emails - - def get_recipient_message(self, address, mailid): - """Retrieve a message for a given recipient. - """ - return Msgrcpt.objects.get(mail=mailid, rid__email=address) - - def set_msgrcpt_status(self, address, mailid, status): - """Change the status (rs field) of a message recipient. - - :param string status: status - """ - addr = Maddr.objects.get(email=address) - self._exec( - "UPDATE msgrcpt SET rs=%s WHERE mail_id=%s AND rid=%s", - [status, mailid, addr.id] - ) - - def get_domains_pending_requests(self, domains): - """Retrieve pending release requests for a list of domains. - """ - regexp = "(%s)" % '|'.join([dom.name for dom in domains]) - return Msgrcpt.objects.filter(rs='p', rid__email__regex=regexp) - - def get_pending_requests(self): - """Return the number of requests currently pending.""" - rq = Q(rs='p') - if not self.user.is_superuser: - doms = Domain.objects.get_for_admin(self.user) - if not doms.count(): - return 0 - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - doms_q = Q(rid__email__regex=regexp) - rq &= doms_q - return Msgrcpt.objects.filter(rq).count() - - def get_mail_content(self, mailid): - """Retrieve the content of a message.""" - content = "".join([ - str(qmail.mail_text) - for qmail in Quarantine.objects.filter(mail=mailid) - ]) - if isinstance(content, unicode): - content = content.encode("utf-8") - return content - - -class PgSQLconnector(SQLconnector): - - """ - The postgres version. - - Make use of ``QuerySet.extra`` and postgres ``convert_from`` - function to let the quarantine manager work as expected ! - - The T4 alias is not a random choice. The generated query was - dumped to found it. Be careful since it can changes with - future Django versions... - - """ - - def _apply_msgrcpt_filters(self, flt): - """Return filters based on user's role. """ - self._where = [] - if self.user.group == 'SimpleUsers': - rcpts = [self.user.email] - if self.user.mailbox_set.count(): - rcpts += self.user.mailbox_set.all()[0].alias_addresses - self._where.append( - "convert_from(maddr.email, 'UTF8') IN (%s)" - % (','.join(["'%s'" % rcpt for rcpt in rcpts]))) - elif not self.user.is_superuser: - doms = Domain.objects.get_for_admin(self.user) - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - self._where.append( - "convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp) - return flt - - def _apply_extra_search_filter(self, crit, pattern): - """Apply search filters using additional criterias.""" - if crit == "to": - self._where.append( - "convert_from(maddr.email, 'UTF8') LIKE '%%{0}%%'".format( - pattern) - ) - return None - - def _apply_extra_select_filters(self, messages): - """Just a hook to apply additional filters to the queryset. - - """ - return messages.extra(where=self._where) - - def get_recipient_message(self, address, mailid): - """Retrieve a message for a given recipient. - """ - qset = Msgrcpt.objects.filter(mail=mailid).extra( - where=["msgrcpt.rid=maddr.id", - "convert_from(maddr.email, 'UTF8') = '%s'" % address], - tables=['maddr'] - ) - return qset.all()[0] - - def get_domains_pending_requests(self, domains): - """Retrieve pending release requests for a list of domains. - """ - regexp = "(%s)" % '|'.join([dom.name for dom in domains]) - return Msgrcpt.objects.filter(rs='p').extra( - where=["msgrcpt.rid=maddr.id", - "convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp], - tables=['maddr'] - ) - - def get_pending_requests(self): - """Return the number of requests currently pending.""" - rq = Q(rs='p') - if not self.user.is_superuser: - doms = Domain.objects.get_for_admin(self.user) - if not doms.count(): - return 0 - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - return Msgrcpt.objects.filter(rq).extra( - where=["msgrcpt.rid=maddr.id", - "convert_from(maddr.email, 'UTF8') ~ '%s'" % (regexp,)], - tables=['maddr'] - ).count() - return Msgrcpt.objects.filter(rq).count() - - -def get_connector(**kwargs): - """Return the appropriate *connector* class. - - The result depends on the DB engine in use. - """ - if db_type("amavis") == 'postgres': - return PgSQLconnector(**kwargs) - return SQLconnector(**kwargs) diff --git a/modoboa/extensions/amavis/sql_email.py b/modoboa/extensions/amavis/sql_email.py deleted file mode 100644 index 201287462..000000000 --- a/modoboa/extensions/amavis/sql_email.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding: utf-8 -""" -An email representation based on a database record. -""" - -from django.template.loader import render_to_string - -from modoboa.lib.email_utils import Email - -from .sql_connector import get_connector - - -class SQLemail(Email): - - """The SQL version of the Email class.""" - - def __init__(self, *args, **kwargs): - super(SQLemail, self).__init__(*args, **kwargs) - fields = ["From", "To", "Cc", "Date", "Subject"] - for f in fields: - label = f - self.headers += [ - {"name": label, "value": self.get_header(self.msg, f)} - ] - setattr(self, label, self.msg[f]) - qreason = self.get_header(self.msg, "X-Amavis-Alert") - self.qtype = "" - self.qreason = "" - if qreason != "": - if ',' in qreason: - self.qtype, self.qreason = qreason.split(',', 1) - elif qreason.startswith("BAD HEADER SECTION "): - # Workaround for amavis <= 2.8.0 :p - self.qtype = "BAD HEADER SECTION" - self.qreason = qreason[19:] - - @property - def msg(self): - """Get message's content.""" - import email - - if self._msg is None: - mail_text = get_connector().get_mail_content(self.mailid) - self._msg = email.message_from_string(mail_text) - self._parse(self._msg) - return self._msg - - def render_headers(self, **kwargs): - return render_to_string("amavis/mailheaders.html", { - "qtype": self.qtype, "qreason": self.qreason, - "headers": self.headers, - }) diff --git a/modoboa/extensions/amavis/static/amavis/css/quarantine.css b/modoboa/extensions/amavis/static/amavis/css/quarantine.css deleted file mode 100644 index 687d4fab2..000000000 --- a/modoboa/extensions/amavis/static/amavis/css/quarantine.css +++ /dev/null @@ -1,90 +0,0 @@ -@media (max-width: 400px){ - th#rstatus, - td[name='rstatus'], - th#type, - td[name='type']{ - display: none; - } -} -@media (max-width: 600px){ - th#subject, - td[name='subject']{ - display: none; - } -} -@media (max-width: 767px){ - th#to, - td[name='to']{ - display: none; - } -} - -@media (max-width: 990px){ - #filters_bar, - th#date, - td[name='date']{ - display: none; - } -} - -#menubar { - position: absolute; - top: 51px; - padding-left: 15px; - padding-right: 15px; - padding-top: 20px; - padding-bottom: 10px; - left: 0; - right: 0; - margin: 0; - z-index: 10; -} - -#menubar input[type="checkbox"] { - margin: 0; -} - -#listing { - position: absolute; - top: 125px; - left: 0; - right: 0; - bottom: 0; - margin: 0; - overflow: auto; - padding-left: 15px; - padding-right: 15px; -} - -#mailcontent { - border: 0; - margin: 0; - padding: 0; - width: 100%; - height: 100%; -} - -#emails tbody tr:hover { - color: #0088cc; -} - -thead tr { - position: relative; - z-index: 1; -} - -td[name=type] span { - cursor: pointer; -} - -td.unseen { - font-weight: bold; -} - -td[class*=pending] { - font-style: italic; -} - -td[class*=openable] { - cursor: pointer; -} \ No newline at end of file diff --git a/modoboa/extensions/amavis/static/amavis/css/selfservice.css b/modoboa/extensions/amavis/static/amavis/css/selfservice.css deleted file mode 100644 index 393c1c090..000000000 --- a/modoboa/extensions/amavis/static/amavis/css/selfservice.css +++ /dev/null @@ -1,21 +0,0 @@ -#menubar { - padding-left: 15px; - z-index: 10; -} - -#listing { - position: absolute; - padding: 0px; - right: 0; - left: 0; - bottom: 0; - overflow: hidden; -} - -#mailcontent { - border: 0; - margin: 0; - padding: 0; - width: 100%; - height: 100%; -} \ No newline at end of file diff --git a/modoboa/extensions/amavis/static/amavis/js/quarantine.js b/modoboa/extensions/amavis/static/amavis/js/quarantine.js deleted file mode 100644 index 2885c28cb..000000000 --- a/modoboa/extensions/amavis/static/amavis/js/quarantine.js +++ /dev/null @@ -1,489 +0,0 @@ -/** - * Return an instance of Quarantine. - * - * @constructor - * @param {Object} options - instance options - */ -var Quarantine = function(options) { - Listing.call(this, options); -}; - -Quarantine.prototype = { - constructor: Quarantine, - - defaults: { - deflocation: "listing", - main_table_id: "emails", - scroll_container: "#listing", - navigation_params: ["sort_order", "pattern", "criteria", "msgtype"], - eor_message: gettext("No more message to show") - }, - - initialize: function(options) { - this.options = $.extend({}, this.defaults, options); - this.options.defcallback = $.proxy(this.listing_cb, this); - Listing.prototype.initialize.call(this, this.options); - this.register_navcallbacks(); - this.listen(); - this.set_msgtype(); - - $(document).on('click', '#selectall', this.toggle_selection); - this.initialize_searchfield(); - }, - - /** - * Search field initialization. - */ - initialize_searchfield: function() { - $("#searchfield").searchbar({ - navobj: this.navobj, - pattern_changed: function(navobj) { - navobj.setparam("reset_page", "true"); - } - }); - }, - - /** - * Activate the button corresponding to the current message type - * filter. If no message type is found, the 'All' button is - * selected. - */ - set_msgtype: function() { - var msgtype = this.navobj.getparam('msgtype'); - - $("input[name*=msgtype]").attr("checked", false) - .parent().removeClass('active'); - - if (msgtype !== undefined) { - $("input[name=msgtype_" + msgtype + "]").attr("checked", true) - .parent().addClass('active'); - } else { - $("button[name=msgtype_all]").attr("checked", true) - .parent().addClass('active'); - } - }, - - /** - * Update the main page content. - * - * @this Quarantine - * @param {Object} data - new content - */ - update_page: function(data) { - if (data.menu !== undefined) { - $("#menubar").html(data.menu); - this.initialize_searchfield(); - } - - if (data.listing !== undefined) { - $("#listing").html(data.listing); - } - if (this.navobj.getbaseurl() == "listing") { - this.update_listing(data); - $(this.options.scroll_container).scrollTop(10); - } else { - this.update_listing(data, false); - } - }, - - listen: function() { - $(document).on("click", "td[class*=openable]", $.proxy(this.viewmail_loader, this)); - $(document).on("click", "a[name=selectmsgs]", $.proxy(this.selectmsgs, this)); - $(document).on("click", "a[name=release-multi]", - $.proxy(this.release_selection, this)); - $(document).on("click", "a[name=delete-multi]", - $.proxy(this.delete_selection, this)); - $(document).on("click", "a[name=mark-as-spam-multi]", - $.proxy(this.mark_selection_as_spam, this)); - $(document).on("click", "a[name=mark-as-ham-multi]", - $.proxy(this.mark_selection_as_ham, this)); - $(document).on("click", "a[name=viewrequests]", - $.proxy(this.view_requests, this)); - $(document).on("click", "a[name=release]", $.proxy(this.release, this)); - $(document).on("click", "a[name=delete]", $.proxy(this.delete, this)); - $(document).on("click", "a[name=mark-as-spam]", - $.proxy(this.mark_as_spam, this)); - $(document).on("click", "a[name=mark-as-ham]", - $.proxy(this.mark_as_ham, this)); - $(document).on("click", "a[name=headers]", $.proxy(this.headers, this)); - $(document).on("click", "td[name=type] span", $.proxy(this.filter_by_type, this)); - $(document).on("click", "#filters label", $.proxy(this.filter_by_type, this)); - }, - - load_page: function(e) { - Listing.prototype.load_page.apply(this, arguments); - this.navobj.delparam("rcpt").update(); - }, - - /** - * Return extra arguments used to fetch a page. - * - * @this Listing - */ - get_load_page_args: function() { - var args = Listing.prototype.get_load_page_args.call(this); - - args.scroll = true; - return args; - }, - - /** - * Calculate the bottom position of the scroll container. - * - * @param {Object} $element - scroll container object - */ - calculate_bottom: function($element) { - var emails_height = $("#emails").height(); - - if (!emails_height) { - return undefined; - } - return $("#emails").height() - $element.height(); - }, - - view_requests: function(e) { - e.preventDefault(); - this.navobj.setparam("viewrequests", "1").update(); - }, - - viewmail_loader: function(e) { - e.preventDefault(); - var $tr = $(e.target).parent(); - var $to = $tr.find("td[name=to]"); - - this.navobj.baseurl($tr.attr("id")); - if ($to.length) { - this.navobj.setparam("rcpt", $to.html().trim()); - } - this.navobj.update(); - }, - - /* - * Toggle message selection when the top checkbox's state is - * modified. - * - * If it is checked, all messages are selected. Otherwise, the - * current selection is resetted. - */ - toggle_selection: function(e) { - if (!$('#selectall').prop('checked')) { - $("#emails").htmltable("clear_selection"); - return; - } - $("td[name*=selection]").each(function() { - var $input = $(this).children('input'); - $input.prop('checked', true); - $('#emails').htmltable('select_row', $(this).parent()); - }); - }, - - /* - * Select all messages of a specific type. - */ - selectmsgs: function(e) { - e.preventDefault(); - var type = get_target(e, 'a').attr("href"); - var counter = 0; - - $("td[name=type]").each(function() { - var $this = $(this); - if ($this.find('span').html().trim() == type) { - var $input = $this.parent().children('td[name=selection]').children('input'); - $input.prop('checked', true); - $('#emails').htmltable('select_row', $this.parent()); - counter++; - } - }); - if (counter) { - $("#selectall").prop('checked', true); - } - }, - - /** - * Filter listing by message type (spam, virus, etc.) - * - * @param {Object} evt - event object - */ - filter_by_type: function(evt) { - var $target = $(evt.target); - var msgtype; - - if ($target.is("span")) { - msgtype = $target.html().trim(); - } else { - msgtype = $target.children("input").attr("name").replace("msgtype_", ""); - } - - if (msgtype != 'all') { - this.navobj.setparam('msgtype', msgtype); - } else { - this.navobj.delparam('msgtype'); - } - this.navobj.update(); - }, - - /** - * Return the list of currently selected messages. - * - * @return {Array} - list of message IDs - */ - get_current_selection: function() { - var selection = []; - - this.htmltable.current_selection().each(function() { - var $tr = $(this); - var $to = $tr.find("td[name=to]"); - - if ($to.length) { - selection.push($to.html().trim() + " " + $tr.attr("id")); - } else { - selection.push($tr.attr("id")); - } - }); - return selection; - }, - - _send_selection: function(e, name, message) { - var $link = $(e.target); - - e.preventDefault(); - if (!this.htmltable.current_selection().length || - !confirm(message)) { - return; - } - var selection = this.get_current_selection(); - - $.ajax({ - url: $link.attr("href"), - data: "action=" + name + "&selection=" + selection.join(","), - type: 'POST', - dataType: 'json' - }).done( - $.proxy(this.action_cb, this) - ).fail(function(jqxhr) { - var data = $.parseJSON(jqxhr.responseText); - $("body").notify("error", data.message); - }); - }, - - release_selection: function(e) { - this._send_selection(e, "release", gettext("Release this selection?")); - }, - - delete_selection: function(e) { - this._send_selection(e, "delete", gettext("Delete this selection?")); - }, - - /** - * Show the recipient selection form inside a modal box. - * - * @param {Object} evt - event object - * @param {string} ltype - learning type (spam or ham) - */ - show_select_rcpt_form: function(evt, ltype, selection) { - if (selection === undefined) { - selection = this.get_current_selection(); - } - var url = this.options.learning_recipient_url + - "?type=" + ltype + "&selection=" + - selection.join(","); - var $this = this; - - modalbox(evt, null, url, function() { - $(".submit").on("click", function(evt) { - simple_ajax_form_post(evt, { - formid: "learning_recipient_form", - reload_on_success: false, - success_cb: $.proxy($this.action_cb, $this) - }); - }); - }); - }, - - /** - * Mark the current selection as spam. - * - * @param {Object} e - event object - */ - mark_selection_as_spam: function(e) { - if (this.options.check_learning_rcpt && - this.htmltable.current_selection().length) { - this.show_select_rcpt_form(e, "spam"); - } else { - this._send_selection( - e, "mark_as_spam", gettext("Mark this selection as spam?")); - } - }, - - /** - * Mark the current selection as ham. - * - * @param {Object} e - event object - */ - mark_selection_as_ham: function(e) { - if (this.options.check_learning_rcpt && - this.htmltable.current_selection().length) { - this.show_select_rcpt_form(e, "ham"); - } else { - this._send_selection( - e, "mark_as_ham", gettext("Mark this selection as non-spam?")); - } - }, - - _send_action: function(e, message) { - var $link = get_target(e, "a"); - - e.preventDefault(); - if (!confirm(message)) { - return; - } - $.ajax({ - url: $link.attr("href"), - dataType: 'json', - type: 'POST', - data: {rcpt: get_parameter_by_name($link.attr("href"), 'rcpt')} - }).done( - $.proxy(this.action_cb, this) - ).fail(function(jqxhr) { - var data = $.parseJSON(jqxhr.responseText); - $("body").notify("error", data.message); - }); - }, - - release : function(e) { - this._send_action(e, gettext("Release this message?")); - }, - - delete: function(e) { - this._send_action(e, gettext("Delete this message?")); - }, - - /** - * Mark the current message as spam. - * - * @param {Object} e - event object - */ - mark_as_spam: function(e) { - if (this.options.check_learning_rcpt) { - var $link = get_target(e, "a"); - var selection = get_parameter_by_name($link.attr("href"), "rcpt") + - " " + $link.attr("data-mail-id"); - this.show_select_rcpt_form(e, "spam", [selection]); - } else { - this._send_action(e, gettext("Mark as spam?")); - } - }, - - /** - * Mark the current message as ham. - * - * @param {Object} e - event object - */ - mark_as_ham: function(e) { - if (this.options.check_learning_rcpt) { - var $link = get_target(e, "a"); - var selection = get_parameter_by_name($link.attr("href"), "rcpt") + - " " + $link.attr("data-mail-id"); - this.show_select_rcpt_form(e, "ham", [selection]); - } else { - this._send_action(e, gettext("Mark as non-spam?")); - } - }, - - show_rawheaders: function($mailcontent) { - $mailcontent.find("#rawheaders").removeClass("hidden"); - $mailcontent.find("#emailheaders").addClass("hidden"); - }, - - headers: function(e) { - e.preventDefault(); - - var $link = get_target(e, "a"); - var $mailcontent = $("#mailcontent").contents(); - var $headers = $mailcontent.find("#emailheaders"); - var $rawheaders = $mailcontent.find("#rawheaders"); - - if ($headers.hasClass("hidden")) { - $link.html(gettext("View full headers")); - $headers.removeClass("hidden"); - $rawheaders.addClass("hidden"); - return; - } - $link.html(gettext("Hide full headers")); - if (!$rawheaders.length) { - $.ajax({ - url: $link.attr("href"), - global: false - }).done($.proxy(function(data) { - $mailcontent.find("#table-container").append($(data)); - this.show_rawheaders($mailcontent); - }, this)); - return; - } - this.show_rawheaders($mailcontent); - }, - - update_params: function(e) { - var $link = $(e.target); - e.preventDefault(); - this.navobj.updateparams($link.attr("href")).update(); - }, - - register_navcallbacks: function() { - this.navobj.register_callback("_listing", - $.proxy(this.listing_cb, this)); - this.navobj.register_callback("viewmail", - $.proxy(this.viewmail_cb, this)); - }, - - activate_buttons: function($tr) { - $("a[name*=-multi]").removeClass('disabled'); - $("#selectall").prop('checked', true); - }, - - deactivate_buttons: function($tr) { - if (!this.htmltable || !this.htmltable.current_selection().length) { - $("a[name*=-multi]").addClass('disabled'); - $("#selectall").prop('checked', false); - } - }, - - /** - * Navigation callback: listing. - * - * @this Quarantine - * @param {Object} data - ajax call response (JSON) - */ - listing_cb: function(data) { - this.update_page(data); - this.navobj.delparam("rcpt").update(); - this.set_msgtype(); - $("#emails").htmltable({ - row_selected_event: this.activate_buttons, - row_unselected_event: $.proxy(this.deactivate_buttons, this) - }); - this.htmltable = $("#emails").data("htmltable"); - this.deactivate_buttons(); - if (this.navobj.hasparam("reset_page")) { - this.navobj.delparam("reset_page").update(false, true); - } - $("#listing").css("overflow", "auto"); - }, - - viewmail_cb: function(data) { - this.update_page(data); - $("#listing").css("overflow", "hidden"); - }, - - action_cb: function(data) { - if (data.reload) { - this.navobj.update(true); - } - if (data.url) { - this.navobj.parse_string(data.url, true).update(true); - } - $("body").notify("success", data.message, 2000); - } -}; - -Quarantine.prototype = $.extend({}, Listing.prototype, Quarantine.prototype); diff --git a/modoboa/extensions/amavis/static/amavis/js/selfservice.js b/modoboa/extensions/amavis/static/amavis/js/selfservice.js deleted file mode 100644 index d634f9bfa..000000000 --- a/modoboa/extensions/amavis/static/amavis/js/selfservice.js +++ /dev/null @@ -1,30 +0,0 @@ -function send_action(e, message) { - var $link = $(e.target); - - e.preventDefault(); - if (!confirm(message)) { - return; - } - $.ajax({ - url: $link.attr("href"), - dataType: 'json' - }).done(function(data) { - $("body").notify("success", data, 2000); - }); -} - -function release(e) { - send_action(e, gettext("Release this message?")); -} - -function remove(e) { - send_action(e, gettext("Delete this message?")); -} - -$(document).ready(function() { - $("#listing").css({ - top: $("#menubar").outerHeight() + 10 - }); - $(document).on("click", "a[name=release]", release); - $(document).on("click", "a[name=delete]", remove); -}); \ No newline at end of file diff --git a/modoboa/extensions/amavis/templates/amavis/domain_content_filter.html b/modoboa/extensions/amavis/templates/amavis/domain_content_filter.html deleted file mode 100644 index d669b7824..000000000 --- a/modoboa/extensions/amavis/templates/amavis/domain_content_filter.html +++ /dev/null @@ -1,18 +0,0 @@ -{% load i18n form_tags %} -{% render_field form.bypass_virus_checks %} -{% render_field form.bypass_spam_checks %} -
    - -
    - {{ form.spam_tag2_level }} -
    - {% trans "or more is spam" %} -
    -
    -
    - {{ form.spam_kill_level }} -
    - {% trans "or more throw spam message away" %} -
    -{% render_field form.spam_subject_tag2 activator=True activator_label=_("default") activator_size="col-sm-3" %} -{% render_field form.bypass_banned_checks %} diff --git a/modoboa/extensions/amavis/templates/amavis/email_list.html b/modoboa/extensions/amavis/templates/amavis/email_list.html deleted file mode 100644 index 439d214c0..000000000 --- a/modoboa/extensions/amavis/templates/amavis/email_list.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load i18n %} - -
    - - - - - - - - - - - - - - - {% include "amavis/emails_page.html" %} - -
    - {% trans "Score" %} - - {% trans "To" %} - - {% trans "From" %} - - {% trans "Subject" %} - - {% trans "Date" %} -
    -
    diff --git a/modoboa/extensions/amavis/templates/amavis/emails_page.html b/modoboa/extensions/amavis/templates/amavis/emails_page.html deleted file mode 100644 index 1b1233c6e..000000000 --- a/modoboa/extensions/amavis/templates/amavis/emails_page.html +++ /dev/null @@ -1,34 +0,0 @@ -{% load i18n %} -{% load amavis_tags %} -{% load static %} - -{% for email in email_list %} - - - - - - {{ email.type|msgtype_to_html }} - - - {% if email.status == 'R' %} - - {% elif email.status == 'S' %} - - {% elif email.status == 'H' %} - - {% endif %} - - {{ email.score }} - {{ email.to }} - - {{ email.from|truncatechars:30 }} - - - {{ email.subject|truncatechars:30 }} - - - {{ email.date|date:"SHORT_DATETIME_FORMAT" }} - - -{% endfor %} diff --git a/modoboa/extensions/amavis/templates/amavis/getmailcontent.html b/modoboa/extensions/amavis/templates/amavis/getmailcontent.html deleted file mode 100644 index 579454343..000000000 --- a/modoboa/extensions/amavis/templates/amavis/getmailcontent.html +++ /dev/null @@ -1 +0,0 @@ -{% if pre %}
    {% endif %}{{ body }}{% if pre %}
    {% endif %} \ No newline at end of file diff --git a/modoboa/extensions/amavis/templates/amavis/index.html b/modoboa/extensions/amavis/templates/amavis/index.html deleted file mode 100644 index 70a1a2ee7..000000000 --- a/modoboa/extensions/amavis/templates/amavis/index.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "fluid.html" %}{% load i18n amavis_tags %} -{% load static %} - -{% block pagetitle %}{% trans "Quarantine management" %}{% endblock %} - -{% block extra_css %} - - -{% endblock %} - -{% block extra_js %} - - - - - - - - -{% endblock %} - -{% block container_content %} - -
    -{% endblock %} diff --git a/modoboa/extensions/amavis/templates/amavis/mailheaders.html b/modoboa/extensions/amavis/templates/amavis/mailheaders.html deleted file mode 100644 index 0ed8b3d61..000000000 --- a/modoboa/extensions/amavis/templates/amavis/mailheaders.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "common/mailheaders.html" %} -{% block emailheaders %} -{% if qtype %}
    {{ qtype }} {{ qreason }}
    {% endif %} -{{ block.super }} -{% endblock %} diff --git a/modoboa/extensions/amavis/templates/amavis/main_action_bar.html b/modoboa/extensions/amavis/templates/amavis/main_action_bar.html deleted file mode 100644 index 70b5222e6..000000000 --- a/modoboa/extensions/amavis/templates/amavis/main_action_bar.html +++ /dev/null @@ -1,79 +0,0 @@ -{% load lib_tags i18n %} -
    - - - -
    - {% include "common/email_searchbar.html" with extraopts=extraopts %} -
    - -
    -
    - -
    -
    - -
    diff --git a/modoboa/extensions/amavis/templates/amavis/notifications/pending_requests.html b/modoboa/extensions/amavis/templates/amavis/notifications/pending_requests.html deleted file mode 100644 index a4729b339..000000000 --- a/modoboa/extensions/amavis/templates/amavis/notifications/pending_requests.html +++ /dev/null @@ -1,14 +0,0 @@ -{% load i18n lib_tags %}{% load url from future %}{% blocktrans count counter=total %} -{{ counter }} release request is pending for action. -{% plural %} -{{ counter }} release requests are pending for action. -{% endblocktrans %} -{% trans "Sketch:" %} -{% for msg in requests %} -{% trans "From:" %} {{ msg.mail.sid.email }} -{% trans "To:" %} {{ msg.rid.email }} -{% trans "Date:" %} {{ msg.mail.time_num|fromunix|date:"DATETIME_FORMAT" }} -{% trans "Subject:" %} {{ msg.mail.subject }} -{% trans "Act on this message:" %} {{ baseurl }}{% url "amavis:index" %}#{{ msg.mail.mail_id }}/?rcpt={{ msg.rid.email }} -{% endfor %} -{% blocktrans %}Please visit {{ listingurl }} for a full listing.{% endblocktrans %} diff --git a/modoboa/extensions/amavis/templates/amavis/viewheader.html b/modoboa/extensions/amavis/templates/amavis/viewheader.html deleted file mode 100644 index 6e5efab6c..000000000 --- a/modoboa/extensions/amavis/templates/amavis/viewheader.html +++ /dev/null @@ -1,4 +0,0 @@ -
    -{% for k, v in headers %}{{ k }}: {{ v }}
    -{% endfor %}
    -
    diff --git a/modoboa/extensions/amavis/templates/amavis/viewmail_selfservice.html b/modoboa/extensions/amavis/templates/amavis/viewmail_selfservice.html deleted file mode 100644 index 90ce332a0..000000000 --- a/modoboa/extensions/amavis/templates/amavis/viewmail_selfservice.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load i18n lib_tags amavis_tags %}{% load url from future %} -{% load static %} - - - - Modoboa - - - - - - - - - - {% url "django.views.i18n.javascript_catalog" as catalog_url %} - - - - -
    - -
    {{ content }}
    -
    - - diff --git a/modoboa/extensions/amavis/templatetags/__init__.py b/modoboa/extensions/amavis/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/amavis/templatetags/amavis_tags.py b/modoboa/extensions/amavis/templatetags/amavis_tags.py deleted file mode 100644 index f5d1a2c9a..000000000 --- a/modoboa/extensions/amavis/templatetags/amavis_tags.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -Amavis frontend template tags. -""" - -from django import template -from django.core.urlresolvers import reverse -from django.template.loader import render_to_string -from django.utils.safestring import mark_safe -from django.utils.translation import ugettext as _ - -from ..lib import manual_learning_enabled - -register = template.Library() - - -@register.simple_tag -def viewm_menu(user, mail_id, rcpt): - """Menu displayed within the viewmail action.""" - entries = [ - {"name": "back", - "url": "javascript:history.go(-1);", - "img": "fa fa-arrow-left", - "class": "btn-primary", - "label": _("Back")}, - {"name": "release", - "img": "fa fa-check", - "class": "btn-success", - "url": reverse('amavis:mail_release', args=[mail_id]) - + ("?rcpt=%s" % rcpt if rcpt else ""), - "label": _("Release")}, - {"name": "delete", - "class": "btn-danger", - "img": "fa fa-trash", - "url": reverse('amavis:mail_delete', args=[mail_id]) - + ("?rcpt=%s" % rcpt if rcpt else ""), - "label": _("Delete")}, - {"name": "headers", - "class": "btn-default", - "url": reverse('amavis:headers_detail', args=[mail_id]), - "label": _("View full headers")}, - ] - - if manual_learning_enabled(user): - entries.insert(3, { - "name": "process", - "img": "fa fa-cog", - "menu": [ - {"name": "mark-as-spam", - "label": _("Mark as spam"), - "url": reverse( - "amavis:mail_mark_as_spam", args=[mail_id] - ) + ("?rcpt=%s" % rcpt if rcpt else ""), - "extra_attributes": { - "data-mail-id": mail_id - }}, - {"name": "mark-as-ham", - "label": _("Mark as non-spam"), - "url": reverse( - "amavis:mail_mark_as_ham", args=[mail_id] - ) + ("?rcpt=%s" % rcpt if rcpt else ""), - "extra_attributes": { - "data-mail-id": mail_id - }} - ] - }) - - menu = render_to_string('common/buttons_list.html', - {"entries": entries, "extraclasses": "pull-left"}) - - entries = [{"name": "close", - "url": "javascript:history.go(-1);", - "img": "icon-remove"}] - menu += render_to_string( - 'common/buttons_list.html', - {"entries": entries, "extraclasses": "pull-right"} - ) - - return menu - - -@register.simple_tag -def viewm_menu_simple(user, mail_id, rcpt, secret_id=""): - release_url = "{0}?rcpt={1}".format( - reverse("amavis:mail_release", args=[mail_id]), rcpt) - delete_url = "{0}?rcpt={1}".format( - reverse("amavis:mail_delete", args=[mail_id]), rcpt) - if secret_id: - release_url += "&secret_id={0}".format(secret_id) - delete_url += "&secret_id={0}".format(secret_id) - entries = [ - {"name": "release", - "img": "fa fa-check", - "class": "btn-success", - "url": release_url, - "label": _("Release")}, - {"name": "delete", - "img": "fa fa-trash", - "class": "btn-danger", - "url": delete_url, - "label": _("Delete")}, - ] - - return render_to_string('common/buttons_list.html', - {"entries": entries}) - - -@register.simple_tag -def quar_menu(user): - """Render the quarantine listing menu. - - :rtype: str - :return: resulting HTML - """ - extraopts = [{"name": "to", "label": _("To")}] - return render_to_string("amavis/main_action_bar.html", { - "extraopts": extraopts, - "manual_learning": manual_learning_enabled(user) - }) - - -@register.filter -def msgtype_to_html(msgtype): - """Transform a message type to a bootstrap label.""" - if msgtype in ["S", "V"]: - color = 'danger' - elif msgtype == "C": - color = "success" - else: - color = "warning" - return mark_safe( - '%s' % (color, msgtype) - ) diff --git a/modoboa/extensions/amavis/urls.py b/modoboa/extensions/amavis/urls.py deleted file mode 100644 index ae9ace7de..000000000 --- a/modoboa/extensions/amavis/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.conf.urls import patterns, url - -urlpatterns = patterns( - 'modoboa.extensions.amavis.views', - - url(r'^$', 'index', name="index"), - url(r'^listing/$', '_listing', name="_mail_list"), - url(r'^listing/page/$', 'listing_page', name="mail_page"), - url(r'^getmailcontent/(?P[\w\-\+]+)/$', 'getmailcontent', - name="mailcontent_get"), - url(r'^process/$', 'process', name="mail_process"), - url(r'^delete/(?P[\w\-\+]+)/$', 'delete', name="mail_delete"), - url(r'^release/(?P[\w\-\+]+)/$', 'release', - name="mail_release"), - url(r'^markspam/(?P[\w\-\+]+)/$', 'mark_as_spam', - name="mail_mark_as_spam"), - url(r'^markham/(?P[\w\-\+]+)/$', 'mark_as_ham', - name="mail_mark_as_ham"), - url(r'^learning_recipient/$', 'learning_recipient', - name="learning_recipient_set"), - url(r'^(?P[\w\-\+]+)/$', 'viewmail', name="mail_detail"), - url(r'^(?P[\w\-\+]+)/headers/$', 'viewheaders', - name="headers_detail"), -) diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py deleted file mode 100644 index 40fba3722..000000000 --- a/modoboa/extensions/amavis/views.py +++ /dev/null @@ -1,436 +0,0 @@ -# coding: utf-8 -""" -Amavis quarantine views. -""" -import email - -import chardet - -from django.shortcuts import render -from django.http import HttpResponseRedirect, Http404 -from django.template import Template, Context -from django.utils.translation import ugettext as _, ungettext -from django.core.urlresolvers import reverse -from django.contrib.auth.decorators import login_required - -from modoboa.lib import parameters -from modoboa.lib.exceptions import BadRequest -from modoboa.lib.paginator import Paginator -from modoboa.lib.web_utils import ( - getctx, render_to_json_response, _render_to_string -) -from modoboa.extensions.admin.models import Mailbox, Domain -from modoboa.extensions.amavis.templatetags.amavis_tags import ( - quar_menu, viewm_menu -) -from .lib import ( - selfservice, AMrelease, QuarantineNavigationParameters, - SpamassassinClient, manual_learning_enabled -) -from .forms import LearningRecipientForm -from .models import Msgrcpt -from .sql_connector import get_connector -from .sql_email import SQLemail - - -def empty_quarantine(): - """Shortcut to use when no content can be displayed.""" - content = "
    %s
    " % _("Empty quarantine") - ctx = getctx("ok", level=2, listing=content) - return render_to_json_response(ctx) - - -def get_listing_pages(request, connector): - """Return listing pages.""" - paginator = Paginator( - connector.messages_count(), - int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")) - ) - page_id = int(connector.navparams.get("page")) - page = paginator.getpage(page_id) - if not page: - return None - pages = [page] - if not page.has_next and page.has_previous and page.items < 40: - pages = [paginator.getpage(page_id - 1)] + pages - email_list = [] - for page in pages: - email_list += connector.fetch(page.id_start, page.id_stop) - return {"pages": [page.number for page in pages], "rows": email_list} - - -@login_required -def listing_page(request): - """Return a listing page.""" - navparams = QuarantineNavigationParameters(request) - previous_page_id = int(navparams["page"]) if "page" in navparams else None - navparams.store() - - connector = get_connector(user=request.user, navparams=navparams) - context = get_listing_pages(request, connector) - if context is None: - context = {"length": 0} - navparams["page"] = previous_page_id - else: - context["rows"] = _render_to_string( - request, "amavis/emails_page.html", {"email_list": context["rows"]} - ) - return render_to_json_response(context) - - -@login_required -def _listing(request): - """Listing initial view. - - Called the first time the listing page is displayed. - """ - if not request.user.is_superuser and request.user.group != 'SimpleUsers': - if not Domain.objects.get_for_admin(request.user).count(): - return empty_quarantine() - - navparams = QuarantineNavigationParameters(request) - navparams.store() - - connector = get_connector(user=request.user, navparams=navparams) - context = get_listing_pages(request, connector) - if context is None: - return empty_quarantine() - context["listing"] = _render_to_string( - request, "amavis/email_list.html", {"email_list": context["rows"]} - ) - del context["rows"] - if request.session.get('location', 'listing') != 'listing': - context["menu"] = quar_menu(request.user) - request.session["location"] = "listingx" - return render_to_json_response(context) - - -@login_required -def index(request): - """Default view.""" - check_learning_rcpt = "false" - if parameters.get_admin("MANUAL_LEARNING") == "yes": - if request.user.group != "SimpleUsers": - user_level_learning = parameters.get_admin( - "USER_LEVEL_LEARNING") == "yes" - domain_level_learning = parameters.get_admin( - "DOMAIN_LEVEL_LEARNING") == "yes" - if user_level_learning or domain_level_learning: - check_learning_rcpt = "true" - return render(request, "amavis/index.html", dict( - selection="quarantine", check_learning_rcpt=check_learning_rcpt - )) - - -def getmailcontent_selfservice(request, mail_id): - mail = SQLemail(mail_id, mformat="plain", links="0") - return render(request, "common/viewmail.html", { - "headers": mail.render_headers(), - "mailbody": mail.body - }) - - -@selfservice(getmailcontent_selfservice) -def getmailcontent(request, mail_id): - mail = SQLemail(mail_id, mformat="plain", links="0") - return render(request, "common/viewmail.html", { - "headers": mail.render_headers(), - "mailbody": mail.body - }) - - -def viewmail_selfservice(request, mail_id, - tplname="amavis/viewmail_selfservice.html"): - rcpt = request.GET.get("rcpt", None) - secret_id = request.GET.get("secret_id", "") - if rcpt is None: - raise Http404 - content = Template("""{% load url from future %} - -""").render(Context(dict(mail_id=mail_id))) - - return render(request, tplname, dict( - mail_id=mail_id, rcpt=rcpt, secret_id=secret_id, content=content - )) - - -@selfservice(viewmail_selfservice) -def viewmail(request, mail_id): - rcpt = request.GET.get("rcpt", None) - if rcpt is None: - raise BadRequest(_("Invalid request")) - if request.user.email == rcpt: - get_connector().set_msgrcpt_status(rcpt, mail_id, 'V') - elif request.user.mailbox_set.count(): - mb = Mailbox.objects.get(user=request.user) - if rcpt == mb.full_address or rcpt in mb.alias_addresses: - get_connector().set_msgrcpt_status(rcpt, mail_id, 'V') - - content = Template(""" - -""").render(Context({"url": reverse("amavis:mailcontent_get", args=[mail_id])})) - menu = viewm_menu(request.user, mail_id, rcpt) - ctx = getctx("ok", menu=menu, listing=content) - request.session['location'] = 'viewmail' - return render_to_json_response(ctx) - - -@login_required -def viewheaders(request, mail_id): - """Display message headers.""" - content = get_connector().get_mail_content(mail_id) - msg = email.message_from_string(content) - headers = [] - for name, value in msg.items(): - if value: - result = chardet.detect(value) - if result["encoding"] is not None: - value = value.decode(result["encoding"]) - headers += [(name, value)] - return render(request, 'amavis/viewheader.html', { - "headers": headers - }) - - -def check_mail_id(request, mail_id): - if type(mail_id) in [str, unicode]: - if "rcpt" in request.POST: - mail_id = ["%s %s" % (request.POST["rcpt"], mail_id)] - else: - mail_id = [mail_id] - return mail_id - - -def get_user_valid_addresses(user): - """Retrieve all valid addresses of a user.""" - valid_addresses = [] - if user.group == 'SimpleUsers': - valid_addresses.append(user.email) - try: - mb = Mailbox.objects.get(user=user) - except Mailbox.DoesNotExist: - pass - else: - valid_addresses += mb.alias_addresses - return valid_addresses - - -def delete_selfservice(request, mail_id): - rcpt = request.GET.get("rcpt", None) - if rcpt is None: - raise BadRequest(_("Invalid request")) - try: - get_connector().set_msgrcpt_status(rcpt, mail_id, 'D') - except Msgrcpt.DoesNotExist: - raise BadRequest(_("Invalid request")) - return render_to_json_response(_("Message deleted")) - - -@selfservice(delete_selfservice) -def delete(request, mail_id): - """Delete message selection. - - :param str mail_id: message unique identifier - """ - mail_id = check_mail_id(request, mail_id) - connector = get_connector() - valid_addresses = get_user_valid_addresses(request.user) - for mid in mail_id: - r, i = mid.split() - if valid_addresses and r not in valid_addresses: - continue - connector.set_msgrcpt_status(r, i, 'D') - message = ungettext("%(count)d message deleted successfully", - "%(count)d messages deleted successfully", - len(mail_id)) % {"count": len(mail_id)} - return render_to_json_response({ - "message": message, - "url": QuarantineNavigationParameters(request).back_to_listing() - }) - - -def release_selfservice(request, mail_id): - rcpt = request.GET.get("rcpt", None) - secret_id = request.GET.get("secret_id", None) - if rcpt is None or secret_id is None: - raise BadRequest(_("Invalid request")) - connector = get_connector() - try: - msgrcpt = connector.get_recipient_message(rcpt, mail_id) - except Msgrcpt.DoesNotExist: - raise BadRequest(_("Invalid request")) - if secret_id != msgrcpt.mail.secret_id: - raise BadRequest(_("Invalid request")) - if parameters.get_admin("USER_CAN_RELEASE") == "no": - connector.set_msgrcpt_status(rcpt, mail_id, 'p') - msg = _("Request sent") - else: - amr = AMrelease() - result = amr.sendreq(mail_id, secret_id, rcpt) - if result: - connector.set_msgrcpt_status(rcpt, mail_id, 'R') - msg = _("Message released") - else: - raise BadRequest(result) - return render_to_json_response(msg) - - -@selfservice(release_selfservice) -def release(request, mail_id): - """Release message selection. - - :param str mail_id: message unique identifier - """ - mail_id = check_mail_id(request, mail_id) - msgrcpts = [] - connector = get_connector() - valid_addresses = get_user_valid_addresses(request.user) - for mid in mail_id: - r, i = mid.split() - if valid_addresses and r not in valid_addresses: - continue - msgrcpts += [connector.get_recipient_message(r, i)] - if request.user.group == "SimpleUsers" and \ - parameters.get_admin("USER_CAN_RELEASE") == "no": - for msgrcpt in msgrcpts: - connector.set_msgrcpt_status( - msgrcpt.rid.email, msgrcpt.mail.mail_id, 'p' - ) - message = ungettext("%(count)d request sent", - "%(count)d requests sent", - len(mail_id)) % {"count": len(mail_id)} - return render_to_json_response({ - "message": message, - "url": QuarantineNavigationParameters(request).back_to_listing() - }) - - amr = AMrelease() - error = None - for rcpt in msgrcpts: - result = amr.sendreq( - rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email - ) - if result: - connector.set_msgrcpt_status(rcpt.rid.email, rcpt.mail.mail_id, 'R') - else: - error = result - break - - if not error: - message = ungettext("%(count)d message released successfully", - "%(count)d messages released successfully", - len(mail_id)) % {"count": len(mail_id)} - else: - message = error - status = 400 if error else 200 - return render_to_json_response({ - "message": message, - "url": QuarantineNavigationParameters(request).back_to_listing() - }, status=status) - - -def mark_messages(request, selection, mtype, recipient_db=None): - """Mark a selection of messages as spam. - - :param str selection: message unique identifier - :param str mtype: type of marking (spam or ham) - """ - if not manual_learning_enabled(request.user): - return render_to_json_response({"status": "ok"}) - if recipient_db is None: - recipient_db = ( - "user" if request.user.group == "SimpleUsers" else "global" - ) - selection = check_mail_id(request, selection) - connector = get_connector() - saclient = SpamassassinClient(request.user, recipient_db) - for item in selection: - rcpt, mail_id = item.split() - content = connector.get_mail_content(mail_id) - result = saclient.learn_spam(rcpt, content) if mtype == "spam" \ - else saclient.learn_ham(rcpt, content) - if not result: - break - connector.set_msgrcpt_status(rcpt, mail_id, mtype[0].upper()) - if saclient.error is None: - saclient.done() - message = ungettext("%(count)d message processed successfully", - "%(count)d messages processed successfully", - len(selection)) % {"count": len(selection)} - else: - message = saclient.error - status = 400 if saclient.error else 200 - return render_to_json_response({ - "message": message, "reload": True - }, status=status) - - -@login_required -def learning_recipient(request): - """A view to select the recipient database of a learning action.""" - if request.method == "POST": - form = LearningRecipientForm(request.user, request.POST) - if form.is_valid(): - return mark_messages( - request, - form.cleaned_data["selection"].split(","), - form.cleaned_data["ltype"], - form.cleaned_data["recipient"] - ) - return render_to_json_response( - {"form_errors": form.errors}, status=400 - ) - ltype = request.GET.get("type", None) - selection = request.GET.get("selection", None) - if ltype is None or selection is None: - raise BadRequest - form = LearningRecipientForm(request.user) - form.fields["ltype"].initial = ltype - form.fields["selection"].initial = selection - return render(request, "common/generic_modal_form.html", { - "title": _("Select a database"), - "formid": "learning_recipient_form", - "action": reverse("amavis:learning_recipient_set"), - "action_classes": "submit", - "action_label": _("Validate"), - "form": form - }) - - -@login_required -def mark_as_spam(request, mail_id): - """Mark a single message as spam.""" - return mark_messages(request, mail_id, "spam") - - -@login_required -def mark_as_ham(request, mail_id): - """Mark a single message as ham.""" - return mark_messages(request, mail_id, "ham") - - -@login_required -def process(request): - """Process a selection of messages. - - The request must specify an action to execute against the - selection. - - """ - action = request.POST.get("action", None) - ids = request.POST.get("selection", "") - ids = ids.split(",") - if not ids or action is None: - return HttpResponseRedirect(reverse("amavis:index")) - - if request.POST["action"] == "release": - return release(request, ids) - - if request.POST["action"] == "delete": - return delete(request, ids) - - if request.POST["action"] == "mark_as_spam": - return mark_messages(request, ids, "spam") - - if request.POST["action"] == "mark_as_ham": - return mark_messages(request, ids, "ham") diff --git a/modoboa/extensions/limits/__init__.py b/modoboa/extensions/limits/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/limits/app_settings.py b/modoboa/extensions/limits/app_settings.py deleted file mode 100644 index 2d759a3cd..000000000 --- a/modoboa/extensions/limits/app_settings.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Custom settings.""" - -from django import forms -from django.utils.translation import ugettext_lazy as _ - -from modoboa.lib.form_utils import SeparatorField -from modoboa.lib.parameters import AdminParametersForm - - -class ParametersForm(AdminParametersForm): - app = "limits" - - defv_sep = SeparatorField(label=_("Default limits")) - - deflt_domain_admins_limit = forms.IntegerField( - label=_("Domain admins"), - initial=0, - help_text=_( - "Maximum number of allowed domain administrators for a new " - "administrator" - ), - widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}) - ) - deflt_domains_limit = forms.IntegerField( - label=_("Domains"), - initial=0, - help_text=_( - "Maximum number of allowed domains for a new administrator"), - widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}) - ) - deflt_domain_aliases_limit = forms.IntegerField( - label=_("Domain aliases"), - initial=0, - help_text=_( - "Maximum number of allowed domain aliases for a new administrator"), - widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}) - ) - deflt_mailboxes_limit = forms.IntegerField( - label=_("Mailboxes"), - initial=0, - help_text=_( - "Maximum number of allowed mailboxes for a new administrator"), - widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}) - ) - deflt_mailbox_aliases_limit = forms.IntegerField( - label=_("Mailbox aliases"), - initial=0, - help_text=_( - "Maximum number of allowed aliases for a new administrator"), - widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}) - ) - - def __init__(self, *args, **kwargs): - super(AdminParametersForm, self).__init__(*args, **kwargs) - self._load_extra_parameters('A') diff --git a/modoboa/extensions/limits/controls.py b/modoboa/extensions/limits/controls.py deleted file mode 100644 index f5d00b4ac..000000000 --- a/modoboa/extensions/limits/controls.py +++ /dev/null @@ -1,219 +0,0 @@ -# coding: utf-8 -""" -:mod:`controls` --- provides event handlers that check if limits are reached ----------------------------------------------------------------------------- - -""" -from .lib import LimitReached, inc_limit_usage, dec_limit_usage -from .models import LimitTemplates, LimitsPool -from modoboa.lib import events -from modoboa.lib.permissions import get_object_owner - - -def check_limit(user, lname, count=1): - """Check if a user has reached a defined limit - - We check if the user will not reach the given limit after this - creation. - - :param user: a ``User`` object - :param int count: the number of objects the user tries to create - :raises: ``LimitReached`` - """ - try: - if user.limitspool.will_be_reached(lname, count): - raise LimitReached(user.limitspool.get_limit(lname)) - except LimitsPool.DoesNotExist: - pass - - -def move_pool_resource(owner, user): - """Move resource from one pool to another - - When an account doesn't need a pool anymore, we move the - associated resource to the pool of its owner. - """ - try: - pool = user.limitspool - except LimitsPool.DoesNotExist: - return - if not owner.is_superuser: - for ltpl in LimitTemplates().templates: - l = user.limitspool.get_limit(ltpl[0]) - if l.maxvalue < 0: - continue - ol = owner.limitspool.get_limit(ltpl[0]) - ol.curvalue += l.curvalue - ol.maxvalue += l.maxvalue - ol.save() - - user.limitspool.delete() - - -@events.observe('DomainCreated') -def inc_nb_domains(user, domain): - inc_limit_usage(user, 'domains_limit') - - -@events.observe('DomainDeleted') -def dec_nb_domains(domain, owner=None): - if owner is None: - owner = get_object_owner(domain) - dec_limit_usage(owner, 'domains_limit') - for domalias in domain.domainalias_set.all(): - dec_nb_domaliases(domalias) - for mailbox in domain.mailbox_set.all(): - dec_nb_mailboxes(mailbox) - for mbalias in domain.alias_set.all(): - dec_nb_mbaliases(mbalias) - - -@events.observe('DomainAliasCreated') -def inc_nb_domaliases(user, domalias): - inc_limit_usage(user, 'domain_aliases_limit') - - -@events.observe('DomainAliasDeleted') -def dec_nb_domaliases(domainaliases): - from modoboa.extensions.admin.models import DomainAlias - - if isinstance(domainaliases, DomainAlias): - domainaliases = [domainaliases] - for domainalias in domainaliases: - owner = get_object_owner(domainalias) - dec_limit_usage(owner, 'domain_aliases_limit') - - -@events.observe('MailboxCreated') -def inc_nb_mailboxes(user, mailbox): - inc_limit_usage(user, 'mailboxes_limit') - - -@events.observe('MailboxDeleted') -def dec_nb_mailboxes(mailboxes): - from modoboa.extensions.admin.models import Mailbox - - if isinstance(mailboxes, Mailbox): - mailboxes = [mailboxes] - for mailbox in mailboxes: - owner = get_object_owner(mailbox) - dec_limit_usage(owner, 'mailboxes_limit') - - -@events.observe('MailboxAliasCreated') -def inc_nb_mbaliases(user, mailboxalias): - inc_limit_usage(user, 'mailbox_aliases_limit') - - -@events.observe('MailboxAliasDeleted') -def dec_nb_mbaliases(mailboxaliases): - from modoboa.extensions.admin.models import Alias - - if isinstance(mailboxaliases, Alias): - mailboxaliases = [mailboxaliases] - for alias in mailboxaliases: - owner = get_object_owner(alias) - dec_limit_usage(owner, 'mailbox_aliases_limit') - - -@events.observe('CanCreate') -def can_create_new_object(user, objtype, count=1): - check_limit(user, '%s_limit' % objtype, count) - - -@events.observe("AccountCreated") -def create_pool(user): - owner = get_object_owner(user) - if owner.group not in ['SuperAdmins', 'Resellers']: - return - - if user.group == 'DomainAdmins': - check_limit(owner, 'domain_admins_limit') - inc_limit_usage(owner, 'domain_admins_limit') - - if user.group in ['DomainAdmins', 'Resellers']: - p, created = LimitsPool.objects.get_or_create(user=user) - p.create_limits(owner) - - -@events.observe("UserCanSetRole") -def user_can_set_role(user, role, account=None): - """Check if the user can still set this role. - - The only interesting case concerns resellers defining new domain - administrators. We want to check if they are allowed to do this - operation before any modification is made to :keyword:`account`. - - :param ``User`` user: connected user - :param ``User`` account: account modified (None on creation) - :param str newrole: role to check - """ - if role == 'DomainAdmins': - lname = 'domain_admins_limit' - else: - return [True] - if user.is_superuser or not user.limitspool.will_be_reached(lname): - return [True] - if account is not None and account.group == role: - return [True] - return [False] - - -@events.observe("AccountModified") -def on_account_modified(old, new): - """Update limits when roles are updated""" - owner = get_object_owner(old) - if owner.group not in ["SuperAdmins", "Resellers"]: - # Domain admins can't change the role so nothing to check. - return - - if new.group != "SuperAdmins": - # Check if account needs a pool (case: a superadmin is - # downgraded) - try: - pool = new.limitspool - except LimitsPool.DoesNotExist: - p = LimitsPool(user=new) - p.save() - p.create_limits(owner) - - if new.group not in ["DomainAdmins", "Resellers"]: - move_pool_resource(owner, new) - - if old.oldgroup == "DomainAdmins": - if new.group != "DomainAdmins": - dec_limit_usage(owner, 'domain_admins_limit') - return - - if new.group == "DomainAdmins": - check_limit(owner, 'domain_admins_limit') - inc_limit_usage(owner, 'domain_admins_limit') - - -@events.observe("AccountDeleted") -def on_account_deleted(account, byuser, **kwargs): - owner = get_object_owner(account) - if owner.group not in ["SuperAdmins", "Resellers"]: - return - - move_pool_resource(owner, account) - - if account.group == "DomainAdmins": - dec_limit_usage(owner, 'domain_admins_limit') - - -@events.observe('DomainOwnershipRemoved') -def domain_ownership_removed(reseller, domain): - """DomainOwnershipRemoved listener. - - The access :keyword:`reseller` had to :keyword:`domain` has been - removed by a super adminstrator. We must decrement all limit - usages. - - :param ``User`` reseller: reseller that created :keyword:`domain` - :param ``Domain`` domain: domain - """ - dec_nb_domains(domain, reseller) - for dadmin in domain.admins: - if reseller.is_owner(dadmin): - dec_limit_usage(reseller, 'domain_admins_limit') diff --git a/modoboa/extensions/limits/fixtures/demo.json b/modoboa/extensions/limits/fixtures/demo.json deleted file mode 100644 index 52a9bc1a1..000000000 --- a/modoboa/extensions/limits/fixtures/demo.json +++ /dev/null @@ -1 +0,0 @@ -[{"pk": 1, "model": "limits.limitspool", "fields": {"user": 2}}, {"pk": 1, "model": "limits.limit", "fields": {"curvalue": 0, "name": "domain_admins_limit", "pool": 1, "maxvalue": -2}}, {"pk": 2, "model": "limits.limit", "fields": {"curvalue": 0, "name": "domains_limit", "pool": 1, "maxvalue": -2}}, {"pk": 3, "model": "limits.limit", "fields": {"curvalue": 0, "name": "domain_aliases_limit", "pool": 1, "maxvalue": -2}}, {"pk": 4, "model": "limits.limit", "fields": {"curvalue": 0, "name": "mailboxes_limit", "pool": 1, "maxvalue": 5}}, {"pk": 5, "model": "limits.limit", "fields": {"curvalue": 0, "name": "mailbox_aliases_limit", "pool": 1, "maxvalue": 5}},{"pk": 3, "model": "auth.group", "fields": {"name": "Resellers", "permissions": [["add_user", "core", "user"], ["change_user", "core", "user"], ["delete_user", "core", "user"], ["view_domains", "admin", "domain"], ["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"], ["view_domains", "admin", "domain"], ["add_domain", "admin", "domain"], ["change_domain", "admin", "domain"], ["delete_domain", "admin", "domain"]]}}] \ No newline at end of file diff --git a/modoboa/extensions/limits/forms.py b/modoboa/extensions/limits/forms.py deleted file mode 100644 index b96d10ef3..000000000 --- a/modoboa/extensions/limits/forms.py +++ /dev/null @@ -1,91 +0,0 @@ -# coding: utf-8 - -"""Custom forms.""" - -from django import forms -from django.utils.translation import ugettext as _ - -from .lib import BadLimitValue, UnsufficientResource -from .models import LimitTemplates -from modoboa.lib import parameters - - -class ResourcePoolForm(forms.Form): - - def __init__(self, *args, **kwargs): - self.account = None - if "instance" in kwargs: - self.account = kwargs["instance"] - del kwargs["instance"] - super(ResourcePoolForm, self).__init__(*args, **kwargs) - for tpl in LimitTemplates().templates: - if len(tpl) > 3: - if self.account is not None and self.account.group != tpl[3]: - continue - self.fields[tpl[0]] = forms.IntegerField( - label=tpl[1], help_text=tpl[2] - ) - if hasattr(self, "account"): - self.load_from_user(self.account) - - def check_limit_value(self, lname): - if self.cleaned_data[lname] < -1: - raise forms.ValidationError(_("Invalid limit")) - return self.cleaned_data[lname] - - def clean(self): - cleaned_data = super(ResourcePoolForm, self).clean() - for lname in self.fields.keys(): - if lname not in self._errors and cleaned_data[lname] < -1: - self.add_error(lname, _("Invalid limit")) - return cleaned_data - - def load_from_user(self, user): - for lname in self.fields.keys(): - self.fields[lname].initial = user.limitspool.getmaxvalue(lname) - # The following lines will become useless in a near - # future. - if self.fields[lname].initial == -2: - self.fields[lname].initial = parameters.get_admin( - "DEFLT_%s" % - lname.upper()) - - def allocate_from_pool(self, limit, pool): - """Allocate resource using an existing pool. - - When a reseller creates a domain administrator, he generally - assigns him resource to create new objetcs. As a reseller may - also be limited, the resource he gives is taken from its own - pool. - """ - ol = pool.get_limit(limit.name) - if ol.maxvalue == -2: - raise BadLimitValue(_("Your resources are not initialized yet")) - newvalue = self.cleaned_data[limit.name] - if newvalue == -1 and ol.maxvalue != -1: - raise BadLimitValue( - _("You're not allowed to define unlimited values") - ) - - if limit.maxvalue > -1: - newvalue -= limit.maxvalue - if newvalue == 0: - return - remain = ol.maxvalue - ol.curvalue - if newvalue > remain: - raise UnsufficientResource(ol) - ol.maxvalue -= newvalue - ol.save() - - def save(self): - from modoboa.lib.permissions import get_object_owner - - owner = get_object_owner(self.account) - for ltpl in LimitTemplates().templates: - if ltpl[0] not in self.cleaned_data: - continue - l = self.account.limitspool.limit_set.get(name=ltpl[0]) - if not owner.is_superuser: - self.allocate_from_pool(l, owner.limitspool) - l.maxvalue = self.cleaned_data[ltpl[0]] - l.save() diff --git a/modoboa/extensions/limits/general_callbacks.py b/modoboa/extensions/limits/general_callbacks.py deleted file mode 100644 index 3f207eebc..000000000 --- a/modoboa/extensions/limits/general_callbacks.py +++ /dev/null @@ -1,123 +0,0 @@ -"""General event callbacks.""" - -from django.utils.translation import ugettext as _ - -from modoboa.core import PERMISSIONS as CORE_PERMS -from modoboa.extensions.admin.modo_extension import PERMISSIONS as ADMIN_PERMS -from modoboa.extensions.limits.forms import ResourcePoolForm -from modoboa.extensions.limits.models import LimitTemplates -from modoboa.lib import events - - -PERMISSIONS = { - "Resellers": ( - CORE_PERMS.get("DomainAdmins") + - ADMIN_PERMS.get("DomainAdmins") + - [["admin", "domain", "view_domains"], - ["admin", "domain", "add_domain"], - ["admin", "domain", "change_domain"], - ["admin", "domain", "delete_domain"]] - ) -} - - -@events.observe("GetExtraRoles") -def get_extra_roles(user): - if user.is_superuser: - return [("Resellers", _("Reseller")), ] - return [] - - -@events.observe("GetExtraRolePermissions") -def extra_permissions(rolename): - """Return extra permissions for Resellers.""" - return PERMISSIONS.get(rolename, []) - - -@events.observe("ExtraAdminContent") -def display_pool_usage(user, target, currentpage): - from django.template.loader import render_to_string - - if target != "leftcol" or user.is_superuser: - return [] - if currentpage == "identities": - names = ["mailboxes_limit", "mailbox_aliases_limit"] - if user.has_perm("admin.add_domain"): - names += ["domain_admins_limit"] - else: - names = [ - tpl[0] for tpl in LimitTemplates().templates - if tpl[0] not in ["domain_admins_limit", "mailboxes_limit", - "mailbox_aliases_limit"] - and (len(tpl) == 3 or tpl[3] == user.group) - ] - - limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0) - if len(limits) == 0: - return [] - return [render_to_string("limits/poolusage.html", dict(limits=limits))] - - -@events.observe("ExtraAccountForm") -def extra_account_form(user, account=None): - if user.group not in ["SuperAdmins", "Resellers"]: - return [] - if account is not None and \ - account.group not in ["Resellers", "DomainAdmins"]: - return [] - - return [ - dict( - id="resources", title=_("Resources"), cls=ResourcePoolForm - ) - ] - - -@events.observe("CheckExtraAccountForm") -def check_form_access(account, form): - if form["id"] != "resources": - return [True] - if not account.belongs_to_group("Resellers") and \ - not account.belongs_to_group("DomainAdmins"): - return [False] - return [True] - - -@events.observe("FillAccountInstances") -def fill_account_instances(user, account, instances): - if not user.is_superuser and not user.belongs_to_group("Resellers"): - return - if not account.belongs_to_group("Resellers") and \ - not account.belongs_to_group("DomainAdmins"): - return - instances["resources"] = account - - -@events.observe("GetStaticContent") -def get_static_content(caller, st_type, user): - if caller not in ['domains', 'identities']: - return [] - if user.group == "SimpleUsers": - return [] - if st_type == "css": - return [""" -"""] - return [""" - -"""] diff --git a/modoboa/extensions/limits/lib.py b/modoboa/extensions/limits/lib.py deleted file mode 100644 index c0135a234..000000000 --- a/modoboa/extensions/limits/lib.py +++ /dev/null @@ -1,53 +0,0 @@ -# coding: utf-8 -""" -:mod:`lib` --- public functions -------------------------------- - -""" -from django.utils.translation import ugettext as _ - -from modoboa.extensions.limits.models import LimitsPool -from modoboa.lib.exceptions import ModoboaException - - -class LimitReached(ModoboaException): - http_code = 403 - - def __init__(self, limit): - self.limit = limit - - def __str__(self): - return _("%s: limit reached") % self.limit.label - - -class UnsufficientResource(ModoboaException): - http_code = 424 - - def __init__(self, limit): - self.limit = limit - - def __str__(self): - return _("Not enough resources") - - -class BadLimitValue(ModoboaException): - http_code = 400 - - -def inc_limit_usage(user, lname): - """Increase a given limit usage. - - """ - try: - user.limitspool.inc_curvalue(lname) - except LimitsPool.DoesNotExist: - pass - - -def dec_limit_usage(user, lname): - if user is None: - return - try: - user.limitspool.dec_curvalue(lname) - except LimitsPool.DoesNotExist: - pass diff --git a/modoboa/extensions/limits/migrations/0001_initial.py b/modoboa/extensions/limits/migrations/0001_initial.py deleted file mode 100644 index 82ed516e3..000000000 --- a/modoboa/extensions/limits/migrations/0001_initial.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Limit', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=255)), - ('curvalue', models.IntegerField(default=0)), - ('maxvalue', models.IntegerField(default=-2)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='LimitsPool', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.AddField( - model_name='limit', - name='pool', - field=models.ForeignKey(to='limits.LimitsPool'), - preserve_default=True, - ), - migrations.AlterUniqueTogether( - name='limit', - unique_together=set([('name', 'pool')]), - ), - ] diff --git a/modoboa/extensions/limits/migrations/__init__.py b/modoboa/extensions/limits/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/limits/models.py b/modoboa/extensions/limits/models.py deleted file mode 100644 index 60ced9325..000000000 --- a/modoboa/extensions/limits/models.py +++ /dev/null @@ -1,148 +0,0 @@ -# coding: utf-8 - -"""Models for the limits extensions.""" - -from django.conf import settings -from django.db import models -from django.utils.translation import ugettext as _, ugettext_lazy - -from modoboa.lib import parameters, events -from modoboa.lib.singleton import Singleton - - -class LimitTemplates(Singleton): - - def __init__(self): - self.__templates = [ - ("domain_admins_limit", ugettext_lazy("Domain admins"), - ugettext_lazy("Maximum number of domain administrators this user " - "can create"), - 'Resellers'), - ("domains_limit", ugettext_lazy("Domains"), - ugettext_lazy("Maximum number of domains this user can create"), - 'Resellers'), - ("domain_aliases_limit", ugettext_lazy("Domain aliases"), - ugettext_lazy( - "Maximum number of domain aliases this user can create"), - 'Resellers'), - ("mailboxes_limit", ugettext_lazy("Mailboxes"), - ugettext_lazy("Maximum number of mailboxes this user can create")), - ("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"), - ugettext_lazy("Maximum number of mailbox aliases this user " - "can create")) - ] - - @property - def templates(self): - return self.__templates + \ - events.raiseQueryEvent('GetExtraLimitTemplates') - - -class LimitsPool(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL) - - def create_limits(self, creator): - """Create limits for this pool. - - All limits defined into ``LimitTemplates`` will be created. If - the creator is a super administrator, the default maximum values - will be used. - - :param ``User`` creator: user creating this pool - """ - for ltpl in LimitTemplates().templates: - try: - Limit.objects.get(name=ltpl[0], pool=self) - except Limit.DoesNotExist: - maxvalue = ( - int(parameters.get_admin("DEFLT_%s" % ltpl[0].upper())) - if creator.is_superuser else 0 - ) - Limit.objects.create( - name=ltpl[0], pool=self, maxvalue=maxvalue - ) - - def getcurvalue(self, lname): - l = self.limit_set.get(name=lname) - return l.curvalue - - def getmaxvalue(self, lname): - l = self.limit_set.get(name=lname) - return l.maxvalue - - def set_maxvalue(self, lname, value): - l = self.limit_set.get(name=lname) - l.maxvalue = value - l.save() - - def inc_curvalue(self, lname, nb=1): - l = self.limit_set.get(name=lname) - l.curvalue += nb - l.save() - - def dec_curvalue(self, lname, nb=1): - l = self.limit_set.get(name=lname) - if not l.curvalue: - return - l.curvalue -= nb - l.save() - - def dec_limit(self, lname, nb=1): - l = self.limit_set.get(name=lname) - l.curvalue -= nb - if l.maxvalue > -1: - l.maxvalue -= nb - l.save() - - def inc_limit(self, lname, nb=1): - l = self.limit_set.get(name=lname) - l.curvalue += nb - if l.maxvalue > -1: - l.maxvalue += nb - l.save() - - def will_be_reached(self, lname, nb=1): - l = self.limit_set.get(name=lname) - if l.maxvalue <= -1: - return False - if l.curvalue + nb > l.maxvalue: - return True - return False - - def get_limit(self, lname): - try: - return self.limit_set.get(name=lname) - except Limit.DoesNotExist: - return None - - -class Limit(models.Model): - name = models.CharField(max_length=255) - curvalue = models.IntegerField(default=0) - maxvalue = models.IntegerField(default=-2) - pool = models.ForeignKey(LimitsPool) - - class Meta: - unique_together = (("name", "pool"),) - - @property - def usage(self): - if self.maxvalue < 0: - return -1 - if self.maxvalue == 0: - return 0 - return int(float(self.curvalue) / self.maxvalue * 100) - - @property - def label(self): - for l in LimitTemplates().templates: - if l[0] == self.name: - return l[1] - return "" - - def __str__(self): - if self.maxvalue == -2: - return _("undefined") - if self.maxvalue == -1: - return _("unlimited") - return "%d%%" % self.usage diff --git a/modoboa/extensions/limits/modo_extension.py b/modoboa/extensions/limits/modo_extension.py deleted file mode 100644 index 45a8659a6..000000000 --- a/modoboa/extensions/limits/modo_extension.py +++ /dev/null @@ -1,44 +0,0 @@ -# coding: utf-8 - -""" -The *limits* extension ----------------------- -""" -from django.utils.translation import ugettext_lazy - -from modoboa.core.extensions import ModoExtension, exts_pool -from modoboa.core.models import User -from modoboa.lib import events, parameters -from .models import LimitTemplates, LimitsPool, Limit - - -EVENTS = [ - 'GetExtraLimitTemplates' -] - - -class Limits(ModoExtension): - name = "limits" - label = "Limits" - version = "1.0" - description = ugettext_lazy( - "Per administrator resources to limit the number of objects " - "they can create" - ) - - def load(self): - from modoboa.extensions.limits.app_settings import ParametersForm - from modoboa.extensions.limits import controls - - parameters.register(ParametersForm, ugettext_lazy("Limits")) - events.declare(EVENTS) - from modoboa.extensions.limits import general_callbacks - - def load_initial_data(self): - """Create pools for existing accounts.""" - for user in User.objects.filter(groups__name="DomainAdmins"): - pool, created = LimitsPool.objects.get_or_create(user=user) - for tpl in LimitTemplates().templates: - Limit.objects.get_or_create(name=tpl[0], pool=pool, maxvalue=0) - -exts_pool.register_extension(Limits) diff --git a/modoboa/extensions/limits/templates/limits/poolusage.html b/modoboa/extensions/limits/templates/limits/poolusage.html deleted file mode 100644 index 1665b6d31..000000000 --- a/modoboa/extensions/limits/templates/limits/poolusage.html +++ /dev/null @@ -1,12 +0,0 @@ -{% load i18n lib_tags %} - diff --git a/modoboa/extensions/limits/tests.py b/modoboa/extensions/limits/tests.py deleted file mode 100644 index 7fe604635..000000000 --- a/modoboa/extensions/limits/tests.py +++ /dev/null @@ -1,415 +0,0 @@ -"""Test cases for the limits extension.""" - -from django.core.urlresolvers import reverse - -from modoboa.core.factories import UserFactory -from modoboa.core.models import User -from modoboa.extensions.admin.factories import ( - populate_database -) -from modoboa.extensions.admin.models import Alias, Domain -from modoboa.extensions.limits.models import LimitTemplates -from modoboa.lib import parameters -from modoboa.lib.tests import ModoTestCase - - -class PermissionsTestCase(ModoTestCase): - fixtures = ["initial_users.json"] - - def setUp(self): - super(PermissionsTestCase, self).setUp() - populate_database() - - def test_domainadmin_deletes_reseller(self): - """Check if a domain admin can delete a reseller. - - Expected result: no. - """ - values = dict( - username="reseller@test.com", first_name="Reseller", last_name="", - password1="toto", password2="toto", role="Resellers", - is_active=True, email="reseller@test.com", stepid='step2' - ) - self.ajax_post(reverse("admin:account_add"), values) - account = User.objects.get(username="reseller@test.com") - self.clt.logout() - self.clt.login(username="admin@test.com", password="toto") - resp = self.ajax_post( - reverse("admin:account_delete", args=[account.id]), - {}, status=403 - ) - self.assertEqual(resp, "Permission denied") - - def test_delete_forward(self): - """Delete a forward created by a user. - - This test was added to help fixing #689. - """ - self.clt.logout() - self.clt.login(username="user@test2.com", password="toto") - - values = { - "dest": "rcpt@dest.com", - "keepcopies": True - } - self.ajax_post(reverse("user_forward"), values) - - user = User.objects.get(username="user@test2.com") - - self.clt.logout() - self.clt.login(username="admin", password="password") - self.ajax_post(reverse("admin:account_delete", args=[user.pk])) - - -class ResourceTestCase(ModoTestCase): - - fixtures = ["initial_users.json"] - - def setUp(self): - """Custom setUp method. - - The 'limits' is manually loaded to ensure extra parameters - provided by 'postfix_relay_domains' are properly received. - """ - super(ResourceTestCase, self).setUp() - for tpl in LimitTemplates().templates: - parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits') - populate_database() - - def _create_account(self, username, role='SimpleUsers', status=200): - values = dict( - username=username, first_name="Tester", last_name="Toto", - password1="toto", password2="toto", role=role, - quota_act=True, - is_active=True, email=username, stepid='step2', - ) - return self.ajax_post( - reverse("admin:account_add"), values, status - ) - - def _create_alias(self, email, rcpt='user@test.com', status=200): - values = dict( - email=email, recipients=rcpt, enabled=True - ) - return self.ajax_post( - reverse("admin:alias_add"), values, status - ) - - def _create_domain(self, name, status=200, withtpl=False): - values = { - "name": name, "quota": 100, "create_dom_admin": "no", - "create_aliases": "no", "stepid": 'step2' - } - if withtpl: - values['create_dom_admin'] = 'yes' - values['dom_admin_username'] = 'admin' - values['create_aliases'] = 'yes' - return self.ajax_post( - reverse("admin:domain_add"), values, status - ) - - def _domain_alias_operation(self, optype, domain, name, status=200): - dom = Domain.objects.get(name=domain) - values = { - 'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled, - } - aliases = [alias.name for alias in dom.domainalias_set.all()] - if optype == 'add': - aliases.append(name) - else: - aliases.remove(name) - for cpt, alias in enumerate(aliases): - fname = 'aliases' if not cpt else 'aliases_%d' % cpt - values[fname] = alias - self.ajax_post( - reverse("admin:domain_change", args=[dom.id]), - values, status - ) - - def _check_limit(self, name, curvalue, maxvalue): - l = self.user.limitspool.get_limit('%s_limit' % name) - self.assertEqual(l.curvalue, curvalue) - self.assertEqual(l.maxvalue, maxvalue) - - -class DomainAdminTestCase(ResourceTestCase): - - def setUp(self): - super(DomainAdminTestCase, self).setUp() - self.user = User.objects.get(username='admin@test.com') - self.user.limitspool.set_maxvalue('mailboxes_limit', 2) - self.user.limitspool.set_maxvalue('mailbox_aliases_limit', 2) - self.clt.logout() - self.clt.login(username='admin@test.com', password='toto') - - def test_mailboxes_limit(self): - self._create_account('tester1@test.com') - self._check_limit('mailboxes', 1, 2) - self._create_account('tester2@test.com') - self._check_limit('mailboxes', 2, 2) - resp = self._create_account('tester3@test.com', status=403) - self._check_limit('mailboxes', 2, 2) - self.ajax_post( - reverse('admin:account_delete', - args=[User.objects.get(username='tester2@test.com').id]), - {} - ) - self._check_limit('mailboxes', 1, 2) - - def test_aliases_limit(self): - self._create_alias('alias1@test.com') - self._check_limit('mailbox_aliases', 1, 2) - self._create_alias('alias2@test.com') - self._check_limit('mailbox_aliases', 2, 2) - resp = self._create_alias('alias3@test.com', status=403) - self._check_limit('mailbox_aliases', 2, 2) - self.ajax_post( - reverse('admin:alias_delete') + '?selection=%d' - % Alias.objects.get(address='alias2', domain__name='test.com').id, - {} - ) - self._check_limit('mailbox_aliases', 1, 2) - - def test_aliases_limit_through_account_form(self): - user = User.objects.get(username='user@test.com') - values = dict( - username=user.username, role=user.group, - is_active=user.is_active, email=user.email, quota_act=True, - aliases="alias1@test.com", aliases_1="alias2@test.com" - ) - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - Alias.objects.get(address='alias1', domain__name='test.com') - self._check_limit('mailbox_aliases', 2, 2) - - -class ResellerTestCase(ResourceTestCase): - - def setUp(self): - super(ResellerTestCase, self).setUp() - self.user = UserFactory.create( - username='reseller', groups=('Resellers',) - ) - self.clt.logout() - self.clt.login(username='reseller', password='toto') - - def test_domains_limit(self): - self._create_domain('domain1.tld') - self._check_limit('domains', 1, 2) - self._create_domain('domain2.tld') - self._check_limit('domains', 2, 2) - resp = self._create_domain('domain3.tld', 403) - self._check_limit('domains', 2, 2) - self.ajax_post( - reverse('admin:domain_delete', - args=[Domain.objects.get(name='domain2.tld').id]), - {} - ) - self._check_limit('domains', 1, 2) - - def test_domain_aliases_limit(self): - self._create_domain('pouet.com') - self._domain_alias_operation('add', 'pouet.com', 'domain-alias1.tld') - self._check_limit('domain_aliases', 1, 2) - self._domain_alias_operation('add', 'pouet.com', 'domain-alias2.tld') - self._check_limit('domain_aliases', 2, 2) - resp = self._domain_alias_operation( - 'add', 'pouet.com', 'domain-alias3.tld', 403 - ) - self._check_limit('domain_aliases', 2, 2) - self._domain_alias_operation('delete', 'pouet.com', 'domain-alias2.tld') - self._check_limit('domain_aliases', 1, 2) - - def test_domain_admins_limit(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - self._check_limit('domain_admins', 1, 2) - self._create_account('admin2@domain.tld', role='DomainAdmins') - self._check_limit('domain_admins', 2, 2) - resp = self._create_account( - 'admin3@domain.tld', - role='DomainAdmins', - status=400) - self.assertEqual( - resp['form_errors']['role'][0], - 'Select a valid choice. DomainAdmins is not one of the available ' - 'choices.' - ) - self._check_limit('domain_admins', 2, 2) - - self.user.limitspool.set_maxvalue('mailboxes_limit', 3) - self._create_account('user1@domain.tld') - user = User.objects.get(username='user1@domain.tld') - values = { - 'username': user.username, 'role': 'DomainAdmins', - 'quota_act': True, 'is_active': user.is_active, - 'email': user.email - } - resp = self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, status=400 - ) - self.assertEqual( - resp['form_errors']['role'][0], - 'Select a valid choice. DomainAdmins is not one of the available ' - 'choices.' - ) - self._check_limit('domain_admins', 2, 2) - - def test_domain_admin_resource_are_empty(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - domadmin = User.objects.get(username='admin1@domain.tld') - for l in ['mailboxes', 'mailbox_aliases']: - self.assertEqual( - domadmin.limitspool.get_limit('%s_limit' % l).maxvalue, 0 - ) - - def test_domain_admins_limit_from_domain_tpl(self): - self.user.limitspool.set_maxvalue('domains_limit', 3) - self._create_domain('domain1.tld', withtpl=True) - self._create_domain('domain2.tld', withtpl=True) - self._check_limit('domain_admins', 2, 2) - self._check_limit('domains', 2, 3) - resp = self._create_domain('domain3.tld', status=200, withtpl=True) - self._check_limit('domain_admins', 2, 2) - self._check_limit('domains', 3, 3) - - def test_reseller_deletes_domain(self): - """Check if all resources are restored after the deletion. - """ - self._create_domain('domain.tld', withtpl=True) - dom = Domain.objects.get(name="domain.tld") - self.ajax_post( - reverse("admin:domain_delete", args=[dom.id]), - {} - ) - self._check_limit('domains', 0, 2) - self._check_limit('domain_admins', 1, 2) - self._check_limit('mailboxes', 0, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_sadmin_removes_ownership(self): - self._create_domain('domain.tld', withtpl=True) - dom = Domain.objects.get(name="domain.tld") - self.clt.logout() - self.clt.login(username='admin', password='password') - self.ajax_get( - "{0}?domid={1}&daid={2}".format( - reverse('admin:permission_remove'), dom.id, self.user.id - ), {} - ) - self._check_limit('domains', 0, 2) - self._check_limit('domain_admins', 0, 2) - self._check_limit('mailboxes', 0, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_allocate_from_pool(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - - # Give 1 mailbox and 2 aliases to the admin -> should work - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self._check_limit('mailboxes', 1, 1) - self._check_limit('mailbox_aliases', 0, 0) - - # Delete the admin -> resources should go back to the - # reseller's pool - self.ajax_post( - reverse("admin:account_delete", args=[user.id]), - {} - ) - self._check_limit('mailboxes', 0, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_restore_resources(self): - self._create_domain('domain.tld') - dom = Domain.objects.get(name='domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - dom.add_admin(user) - self.clt.logout() - self.clt.login(username='admin1@domain.tld', password='toto') - self._create_account('user1@domain.tld') - self._create_alias('alias1@domain.tld', 'user1@domain.tld') - self._create_alias('alias2@domain.tld', 'user1@domain.tld') - self.clt.logout() - self.clt.login(username='reseller', password='toto') - # Delete the admin -> resources should go back to the - # reseller's pool - self.ajax_post( - reverse("admin:account_delete", args=[user.id]), - {} - ) - self._check_limit('mailboxes', 1, 2) - self._check_limit('mailbox_aliases', 2, 2) - - def test_change_role(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - - # Give 1 mailbox and 2 aliases to the admin -> should work - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 1, 'mailbox_aliases_limit': 2 - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self._check_limit('mailboxes', 1, 1) - self._check_limit('mailbox_aliases', 0, 0) - - # Change admin role to SimpleUser -> resources should go back - # to the reseller. - values = { - 'username': user.username, 'role': 'SimpleUsers', 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self._check_limit('mailboxes', 1, 2) - self._check_limit('mailbox_aliases', 0, 2) - - def test_allocate_too_much(self): - self._create_domain('domain.tld') - self._create_account('admin1@domain.tld', role='DomainAdmins') - user = User.objects.get(username='admin1@domain.tld') - - # Give 2 mailboxes and 3 aliases to the admin -> should fail. - values = { - 'username': user.username, 'role': user.group, 'quota_act': True, - 'is_active': user.is_active, 'email': user.email, - 'mailboxes_limit': 2, 'mailbox_aliases_limit': 3 - } - resp = self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, 424 - ) - self.assertEqual(resp, 'Not enough resources') - self._check_limit('mailboxes', 1, 2) - self._check_limit('mailbox_aliases', 0, 2) diff --git a/modoboa/extensions/limits/urls.py b/modoboa/extensions/limits/urls.py deleted file mode 100644 index 9adc5e04a..000000000 --- a/modoboa/extensions/limits/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.conf.urls import patterns - -urlpatterns = patterns( - 'modoboa.extensions.limits.views', - -) diff --git a/modoboa/extensions/limits/views.py b/modoboa/extensions/limits/views.py deleted file mode 100644 index 57d631c3f..000000000 --- a/modoboa/extensions/limits/views.py +++ /dev/null @@ -1 +0,0 @@ -# coding: utf-8 diff --git a/modoboa/extensions/postfix_autoreply/__init__.py b/modoboa/extensions/postfix_autoreply/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/postfix_autoreply/app_settings.py b/modoboa/extensions/postfix_autoreply/app_settings.py deleted file mode 100644 index 005f29029..000000000 --- a/modoboa/extensions/postfix_autoreply/app_settings.py +++ /dev/null @@ -1,63 +0,0 @@ -# coding: utf-8 - -"""Custom settings.""" - -from django import forms -from django.utils.translation import ugettext_lazy - -from modoboa.lib.form_utils import SeparatorField -from modoboa.lib.parameters import AdminParametersForm - - -class ParametersForm(AdminParametersForm): - app = "postfix_autoreply" - - general_sep = SeparatorField(label=ugettext_lazy("General")) - - autoreplies_timeout = forms.IntegerField( - label=ugettext_lazy("Automatic reply timeout"), - initial=86400, - help_text=ugettext_lazy( - "Timeout in seconds between two auto-replies to the same recipient" - ), - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - - default_subject = forms.CharField( - label=ugettext_lazy("Default subject"), - initial=ugettext_lazy("I'm off"), - help_text=ugettext_lazy( - "Default subject used when an auto-reply message is created " - "automatically" - ), - widget=forms.TextInput(attrs={"class": "form-control"}) - ) - - default_content = forms.CharField( - label=ugettext_lazy("Default content"), - initial=ugettext_lazy( - """I'm currently off. I'll answer as soon as I come back. - -Best regards, -%(name)s -"""), - help_text=ugettext_lazy( - "Default content used when an auto-reply message is created " - "automatically. The '%(name)s' macro will be replaced by the " - "user's full name." - ), - widget=forms.widgets.Textarea(attrs={"class": "form-control"}) - ) - - def clean_default_content(self): - """Check if the provided value is valid. - - Must be a valid format string which will be used with the % - operator. - """ - tpl = self.cleaned_data["default_content"] - try: - test = tpl % {"name": "Antoine Nguyen"} - except (KeyError, ValueError): - raise forms.ValidationError(ugettext_lazy("Invalid syntax")) - return tpl diff --git a/modoboa/extensions/postfix_autoreply/forms.py b/modoboa/extensions/postfix_autoreply/forms.py deleted file mode 100644 index 2252672bf..000000000 --- a/modoboa/extensions/postfix_autoreply/forms.py +++ /dev/null @@ -1,81 +0,0 @@ -# coding: utf-8 - -"""Custom forms.""" - -from collections import OrderedDict - -from django import forms -from django.utils import timezone -from django.utils.translation import ugettext as _, ugettext_lazy - -from .models import ARmessage - - -class ARmessageForm(forms.ModelForm): - - fromdate = forms.DateTimeField( - label=ugettext_lazy('From'), - required=False, - help_text=ugettext_lazy("Activate your auto reply from this date"), - widget=forms.TextInput( - attrs={'class': 'datefield form-control', 'readonly': 'readonly'} - ) - ) - untildate = forms.DateTimeField( - label=ugettext_lazy('Until'), - required=False, - help_text=ugettext_lazy("Activate your auto reply until this date"), - widget=forms.TextInput( - attrs={'class': 'datefield form-control', 'readonly': 'readonly'} - ) - ) - subject = forms.CharField( - widget=forms.TextInput( - attrs={'class': 'form-control'} - ) - ) - content = forms.CharField( - widget=forms.Textarea( - attrs={'class': 'form-control'} - ) - ) - - class Meta: - model = ARmessage - fields = ('subject', 'content', 'enabled') - - def __init__(self, *args, **kwargs): - super(ARmessageForm, self).__init__(*args, **kwargs) - self.fields = OrderedDict( - (key, self.fields[key]) for key in - ['subject', 'content', 'fromdate', 'untildate', 'enabled'] - ) - if 'instance' in kwargs and kwargs['instance'] is not None: - self.fields['fromdate'].initial = \ - kwargs['instance'].fromdate.replace(second=0, microsecond=0) - self.fields['untildate'].initial = kwargs['instance'].untildate - - def clean(self): - """Custom fields validaton. - - We want to be sure that fromdate < untildate and that they are - both in the future ONLY IF the autoreply is beeing activated. - - """ - super(ARmessageForm, self).clean() - if not self.cleaned_data["enabled"]: - return self.cleaned_data - if self.cleaned_data["fromdate"] is not None: - if self.cleaned_data["fromdate"] < timezone.now(): - self.add_error("fromdate", _("This date is over")) - else: - self.cleaned_data['fromdate'] = timezone.now() - if self.cleaned_data["untildate"] is not None: - untildate = self.cleaned_data["untildate"] - if untildate < timezone.now(): - self.add_error("untildate", _("This date is over")) - elif "fromdate" in self.cleaned_data and \ - untildate < self.cleaned_data["fromdate"]: - self.add_error( - "untildate", _("Must be greater than start date")) - return self.cleaned_data diff --git a/modoboa/extensions/postfix_autoreply/general_callbacks.py b/modoboa/extensions/postfix_autoreply/general_callbacks.py deleted file mode 100644 index 450cf0b42..000000000 --- a/modoboa/extensions/postfix_autoreply/general_callbacks.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Event callbacks.""" - -from django.utils import timezone -from django.utils.translation import ugettext_lazy - -from .models import ARmessage -from modoboa.extensions.postfix_autoreply.models import Transport, Alias -from modoboa.lib import events, parameters - - -@events.observe("ExtraUprefsJS") -def extra_js(user): - return ["""function autoreply_cb() { - $('.datefield').datetimepicker({ - format: 'YYYY-MM-DD hh:mm:ss', - language: '%(lang)s' - }); -} -""" % {'lang': parameters.get_user(user, "LANG", app="core")} - ] - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "uprefs_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "autoreply", - "class": "ajaxnav", - "url": "autoreply/", - "label": ugettext_lazy("Auto-reply message")} - ] - - -@events.observe("DomainCreated") -def onDomainCreated(user, domain): - transport = Transport() - transport.domain = "autoreply.%s" % domain.name - transport.method = "autoreply:" - transport.save() - - -@events.observe("DomainModified") -def onDomainModified(domain): - if domain.oldname == domain.name: - return - Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \ - .update(domain="autoreply.%s" % domain.name) - for al in Alias.objects.filter( - full_address__contains="@%s" % domain.oldname): - new_address = al.full_address.replace( - "@%s" % domain.oldname, - "@%s" % domain.name) - al.full_address = new_address - al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name) - al.save() - - -@events.observe("DomainDeleted") -def onDomainDeleted(domain): - Transport.objects.filter(domain="autoreply.%s" % domain.name).delete() - - -@events.observe("MailboxCreated") -def onMailboxCreated(user, mailbox): - alias = Alias() - alias.full_address = mailbox.full_address - alias.autoreply_address = \ - "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) - alias.save() - - -@events.observe("MailboxDeleted") -def onMailboxDeleted(mailboxes): - from modoboa.extensions.admin.models import Mailbox - - if isinstance(mailboxes, Mailbox): - mailboxes = [mailboxes] - for mailbox in mailboxes: - try: - alias = Alias.objects.get(full_address=mailbox.full_address) - except Alias.DoesNotExist: - pass - else: - alias.delete() - - -@events.observe("MailboxModified") -def onMailboxModified(mailbox): - if not hasattr(mailbox, 'old_full_address'): - return - if mailbox.full_address == mailbox.old_full_address: - return - alias = Alias.objects.get(full_address=mailbox.old_full_address) - alias.full_address = mailbox.full_address - alias.autoreply_address = \ - "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) - alias.save() - - -@events.observe("ExtraFormFields") -def extra_mailform_fields(form_name, mailbox=None): - """Define extra fields to include in mail forms. - - For now, only the auto-reply state can be modified. - - :param str form_name: form name (must be 'mailform') - :param Mailbox mailbox: mailbox - """ - from modoboa.lib.form_utils import YesNoField - - if form_name != "mailform": - return [] - status = False - if mailbox is not None and mailbox.armessage_set.count(): - status = mailbox.armessage_set.all()[0].enabled - return [ - ('autoreply', YesNoField( - label=ugettext_lazy("Enable auto-reply"), - initial="yes" if status else "no", - required=False, - help_text=ugettext_lazy("Enable or disable Postfix auto-reply") - )) - ] - - -@events.observe("SaveExtraFormFields") -def save_extra_mailform_fields(form_name, mailbox, values): - """Set the auto-reply status for a mailbox. - - If a corresponding auto-reply message exists, we update its - status. Otherwise, we create a message using default values. - - :param str form_name: form name (must be 'mailform') - :param Mailbox mailbox: mailbox - :param dict values: form values - """ - if form_name != 'mailform': - return - if mailbox.armessage_set.count(): - arm = mailbox.armessage_set.all()[0] - else: - arm = ARmessage(mbox=mailbox) - arm.subject = parameters.get_admin("DEFAULT_SUBJECT") - arm.content = parameters.get_admin("DEFAULT_CONTENT") \ - % {'name': mailbox.user.fullname} - arm.fromdate = timezone.now() - arm.enabled = True if values['autoreply'] == 'yes' else False - arm.save() diff --git a/modoboa/extensions/postfix_autoreply/management/__init__.py b/modoboa/extensions/postfix_autoreply/management/__init__.py deleted file mode 100644 index 394a0a067..000000000 --- a/modoboa/extensions/postfix_autoreply/management/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 diff --git a/modoboa/extensions/postfix_autoreply/management/commands/__init__.py b/modoboa/extensions/postfix_autoreply/management/commands/__init__.py deleted file mode 100644 index 394a0a067..000000000 --- a/modoboa/extensions/postfix_autoreply/management/commands/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 diff --git a/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py b/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py deleted file mode 100644 index 0dd3ecd23..000000000 --- a/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -import sys -import datetime - -from django.core.management.base import BaseCommand, CommandError -from django.utils import timezone - -from modoboa.core.management.commands import CloseConnectionMixin -from modoboa.lib import parameters -from modoboa.lib.email_utils import split_mailbox, sendmail_simple -from modoboa.extensions.admin.models import Mailbox -from ...models import ARmessage, ARhistoric -from ...modo_extension import PostfixAutoreply - - -def send_autoreply(sender, mailbox, armessage): - if armessage.fromdate > timezone.now(): - return - if armessage.untildate is not None \ - and armessage.untildate < timezone.now(): - armessage.enabled = False - armessage.save() - return - - try: - lastar = ARhistoric.objects.get(armessage=armessage.id, sender=sender) - timeout = parameters.get_admin("AUTOREPLIES_TIMEOUT", - app="postfix_autoreply") - delta = datetime.timedelta(seconds=int(timeout)) - now = timezone.make_aware(datetime.datetime.now(), - timezone.get_default_timezone()) - if lastar.last_sent + delta > now: - sys.exit(0) - except ARhistoric.DoesNotExist: - lastar = ARhistoric() - lastar.armessage = armessage - lastar.sender = sender - - sendmail_simple(mailbox.user.encoded_address, sender, armessage.subject, - armessage.content.encode('utf-8')) - - lastar.last_sent = datetime.datetime.now() - lastar.save() - - -class Command(BaseCommand, CloseConnectionMixin): - args = ' ' - help = 'Send autoreply emails' - - def handle(self, *args, **options): - if len(args) < 2: - raise CommandError( - "usage: ./manage.py autoreply ") - - PostfixAutoreply().load() - sender = args[0] - for fulladdress in args[1:]: - address, domain = split_mailbox(fulladdress) - try: - mbox = Mailbox.objects.get(address=address, domain__name=domain) - except Mailbox.DoesNotExist: - print "Unknown recipient %s" % (fulladdress) - continue - try: - armessage = ARmessage.objects.get(mbox=mbox.id, enabled=True) - except ARmessage.DoesNotExist: - continue - - send_autoreply(sender, mbox, armessage) diff --git a/modoboa/extensions/postfix_autoreply/migrations/0001_initial.py b/modoboa/extensions/postfix_autoreply/migrations/0001_initial.py deleted file mode 100644 index 4bb61e930..000000000 --- a/modoboa/extensions/postfix_autoreply/migrations/0001_initial.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('admin', '__first__'), - ] - - operations = [ - migrations.CreateModel( - name='Alias', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('full_address', models.CharField(max_length=255)), - ('autoreply_address', models.CharField(max_length=255)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='ARhistoric', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('last_sent', models.DateTimeField(auto_now=True)), - ('sender', models.CharField(max_length=254)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='ARmessage', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('subject', models.CharField(help_text='The subject that will appear in sent emails', max_length=255, verbose_name='subject')), - ('content', models.TextField(help_text='The content that will appear in sent emails', verbose_name='content')), - ('enabled', models.BooleanField(default=False, help_text='Activate/Deactivate your auto reply', verbose_name='enabled')), - ('fromdate', models.DateTimeField(default=django.utils.timezone.now)), - ('untildate', models.DateTimeField(null=True, blank=True)), - ('mbox', models.ForeignKey(to='admin.Mailbox')), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Transport', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('domain', models.CharField(max_length=300)), - ('method', models.CharField(max_length=255)), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.AddField( - model_name='arhistoric', - name='armessage', - field=models.ForeignKey(to='postfix_autoreply.ARmessage'), - preserve_default=True, - ), - migrations.AlterUniqueTogether( - name='arhistoric', - unique_together=set([('armessage', 'sender')]), - ), - ] diff --git a/modoboa/extensions/postfix_autoreply/migrations/__init__.py b/modoboa/extensions/postfix_autoreply/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/postfix_autoreply/models.py b/modoboa/extensions/postfix_autoreply/models.py deleted file mode 100644 index 0c53a163b..000000000 --- a/modoboa/extensions/postfix_autoreply/models.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.utils import timezone - -from modoboa.extensions.admin.models import Mailbox - - -class Transport(models.Model): - domain = models.CharField(max_length=300) - method = models.CharField(max_length=255) - - -class Alias(models.Model): - full_address = models.CharField(max_length=255) - autoreply_address = models.CharField(max_length=255) - - -class ARmessage(models.Model): - mbox = models.ForeignKey(Mailbox) - subject = models.CharField( - _('subject'), max_length=255, - help_text=_("The subject that will appear in sent emails") - ) - content = models.TextField( - _('content'), - help_text=_("The content that will appear in sent emails") - ) - enabled = models.BooleanField( - _('enabled'), - help_text=_("Activate/Deactivate your auto reply"), - default=False - ) - fromdate = models.DateTimeField(default=timezone.now) - untildate = models.DateTimeField(null=True, blank=True) - - -class ARhistoric(models.Model): - armessage = models.ForeignKey(ARmessage) - last_sent = models.DateTimeField(auto_now=True) - sender = models.CharField(max_length=254) - - class Meta: - unique_together = ("armessage", "sender") diff --git a/modoboa/extensions/postfix_autoreply/modo_extension.py b/modoboa/extensions/postfix_autoreply/modo_extension.py deleted file mode 100644 index 90e861cde..000000000 --- a/modoboa/extensions/postfix_autoreply/modo_extension.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding: utf-8 -""" -Postfix auto-replies plugin. - -This module provides a way to integrate Modoboa auto-reply -functionality into Postfix. - -""" - -from django.utils.translation import ugettext_lazy - -from modoboa.core.extensions import ModoExtension, exts_pool -from modoboa.extensions.admin.models import Domain -from modoboa.lib import events, parameters -from . import general_callbacks -from .models import Transport, Alias - - -class PostfixAutoreply(ModoExtension): - - """ - Auto-reply (vacation) functionality using Postfix. - - """ - name = "postfix_autoreply" - label = "Postfix autoreply" - version = "1.0" - description = ugettext_lazy( - "Auto-reply (vacation) functionality using Postfix") - - def load(self): - from modoboa.extensions.postfix_autoreply.app_settings import ( - ParametersForm - ) - parameters.register( - ParametersForm, ugettext_lazy("Automatic replies")) - - def load_initial_data(self): - """Create records for existing domains.""" - for dom in Domain.objects.all(): - try: - Transport.objects.get(domain="autoreply.%s" % dom.name) - except Transport.DoesNotExist: - general_callbacks.onDomainCreated(None, dom) - else: - continue - - for mb in dom.mailbox_set.all(): - try: - Alias.objects.get(full_address=mb.full_address) - except Alias.DoesNotExist: - general_callbacks.onMailboxCreated(None, mb) - -exts_pool.register_extension(PostfixAutoreply) - - -@events.observe("ExtraUprefsRoutes") -def extra_routes(): - from django.conf.urls import url - - return [ - url(r'^user/autoreply/$', - 'modoboa.extensions.postfix_autoreply.views.autoreply', - name="autoreply") - ] diff --git a/modoboa/extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html b/modoboa/extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html deleted file mode 100644 index f30ca8166..000000000 --- a/modoboa/extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load i18n form_tags %}{% load url from future %} -

    {% trans "Auto-reply message" %} {% trans "Define a message to automatically send when you are off" %}

    -
    -
    {% csrf_token %} - {% render_form form %} -
    - -
    -
    diff --git a/modoboa/extensions/postfix_autoreply/tests.py b/modoboa/extensions/postfix_autoreply/tests.py deleted file mode 100644 index 3e1dd172e..000000000 --- a/modoboa/extensions/postfix_autoreply/tests.py +++ /dev/null @@ -1,125 +0,0 @@ -from django.core.urlresolvers import reverse -from modoboa.core.models import User -from modoboa.lib.tests import ModoTestCase -from modoboa.extensions.admin import factories -from modoboa.extensions.admin.models import ( - Domain -) -from modoboa.extensions.postfix_autoreply.models import ( - Transport, Alias, ARmessage -) - - -class EventsTestCase(ModoTestCase): - fixtures = ['initial_users.json'] - - def setUp(self): - super(EventsTestCase, self).setUp() - factories.populate_database() - - def test_domain_created_event(self): - values = { - "name": "domain.tld", "quota": 100, "create_dom_admin": "no", - "stepid": 'step2' - } - self.ajax_post( - reverse("admin:domain_add"), values - ) - trans = Transport.objects.get(domain='autoreply.domain.tld') - - def test_domain_deleted_event(self): - dom = Domain.objects.get(name="test.com") - trans = Transport.objects.get(domain='autoreply.test.com') - self.ajax_post( - reverse("admin:domain_delete", args=[dom.id]), - {} - ) - with self.assertRaises(Transport.DoesNotExist): - Transport.objects.get(domain='autoreply.test.com') - - def test_domain_modified_event(self): - values = { - "name": "test.fr", "quota": 100, "enabled": True - } - dom = Domain.objects.get(name="test.com") - self.ajax_post( - reverse("admin:domain_change", args=[dom.id]), - values - ) - trans = Transport.objects.get(domain='autoreply.test.fr') - self.assertEqual( - Alias.objects.filter(full_address__contains='@test.fr').count(), 2 - ) - for al in Alias.objects.filter(full_address__contains='@test.fr'): - self.assertIn('autoreply.test.fr', al.autoreply_address) - - def test_mailbox_created_event(self): - values = { - 'username': "tester@test.com", - 'first_name': 'Tester', 'last_name': 'Toto', - 'password1': 'toto', 'password2': 'toto', 'role': 'SimpleUsers', - 'quota_act': True, 'is_active': True, 'email': 'tester@test.com', - 'stepid': 'step2', 'autoreply': 'no' - } - self.ajax_post( - reverse("admin:account_add"), values - ) - al = Alias.objects.get(full_address='tester@test.com') - self.assertEqual( - al.autoreply_address, 'tester@test.com@autoreply.test.com') - arm = ARmessage.objects.get(mbox__address='tester') - - def test_mailbox_deleted_event(self): - account = User.objects.get(username="user@test.com") - self.ajax_post( - reverse("admin:account_delete", args=[account.id]), - {} - ) - with self.assertRaises(Alias.DoesNotExist): - Alias.objects.get(full_address='user@test.com') - with self.assertRaises(ARmessage.DoesNotExist): - ARmessage.objects.get( - mbox__address='user', mbox__domain__name='test.com') - - def test_modify_mailbox_event(self): - values = { - 'username': "leon@test.com", - 'first_name': 'Tester', 'last_name': 'Toto', - 'role': 'SimpleUsers', 'quota_act': True, - 'is_active': True, 'email': 'leon@test.com', - 'autoreply': 'no' - } - account = User.objects.get(username="user@test.com") - self.ajax_post( - reverse("admin:account_change", args=[account.id]), - values - ) - with self.assertRaises(Alias.DoesNotExist): - Alias.objects.get(full_address='user@test.com') - al = Alias.objects.get(full_address='leon@test.com') - - -class FormTestCase(ModoTestCase): - fixtures = ['initial_users.json'] - - def setUp(self): - super(FormTestCase, self).setUp() - factories.populate_database() - - # def test_set_autoreply(self): - # self.clt.get( - # reverse('modoboa.core.views.user.index') - # ) - # values = { - # 'subject': 'test', 'content': "I'm off", "enabled": True - # } - # self.ajax_post( - # reverse('modoboa.extensions.postfix_autoreply.views.autoreply'), - # values - # ) - # account = User.objects.get(username="user@test.com") - # arm = ARmessage.objects.get(mbox=account.mailbox_set.all()[0]) - # self.assertEqual(arm.subject, 'test') - # self.assertTrue(arm.enabled) - # self.assertFalse(arm.untildate) - # self.assertTrue(arm.fromdate) diff --git a/modoboa/extensions/postfix_autoreply/views.py b/modoboa/extensions/postfix_autoreply/views.py deleted file mode 100644 index cbf7befe0..000000000 --- a/modoboa/extensions/postfix_autoreply/views.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 -from datetime import date -from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext as _ -from modoboa.lib.web_utils import ( - render_to_json_response, _render_to_string -) -from modoboa.extensions.admin.lib import needs_mailbox -from modoboa.extensions.admin.models import Mailbox -from .forms import ARmessageForm -from .models import ARmessage - - -@login_required -@needs_mailbox() -def autoreply(request, tplname="postfix_autoreply/autoreply.html"): - mb = Mailbox.objects.get(user=request.user.id) - try: - arm = ARmessage.objects.get(mbox=mb.id) - except ARmessage.DoesNotExist: - arm = None - if request.method == "POST": - if arm: - form = ARmessageForm(request.POST, instance=arm) - else: - form = ARmessageForm(request.POST) - if form.is_valid(): - arm = form.save(commit=False) - arm.fromdate = form.cleaned_data["fromdate"] - arm.untildate = form.cleaned_data["untildate"] - arm.mbox = mb - arm.save() - return render_to_json_response( - _("Auto reply message updated successfully.") - ) - - return render_to_json_response( - {"form_errors": form.errors}, status=400 - ) - - form = ARmessageForm(instance=arm) - return render_to_json_response({ - "content": _render_to_string(request, tplname, {"form": form}), - "onload_cb": "autoreply_cb" - }) diff --git a/modoboa/extensions/postfix_relay_domains/__init__.py b/modoboa/extensions/postfix_relay_domains/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/postfix_relay_domains/amavis_callbacks.py b/modoboa/extensions/postfix_relay_domains/amavis_callbacks.py deleted file mode 100644 index d73770986..000000000 --- a/modoboa/extensions/postfix_relay_domains/amavis_callbacks.py +++ /dev/null @@ -1,35 +0,0 @@ -from modoboa.lib import events -from modoboa.extensions.amavis.lib import ( - create_user_and_policy, update_user_and_policy, delete_user_and_policy, - create_user_and_use_policy, delete_user -) - - -@events.observe("RelayDomainCreated") -def on_relay_domain_created(user, rdomain): - create_user_and_policy("@{0}".format(rdomain.name)) - - -@events.observe("RelayDomainModified") -def on_relay_domain_modified(rdomain): - update_user_and_policy( - "@{0}".format(rdomain.oldname), "@{0}".format(rdomain.name) - ) - - -@events.observe("RelayDomainDeleted") -def on_relay_domain_deleted(rdomain): - delete_user_and_policy("@{0}".format(rdomain.name)) - - -@events.observe("RelayDomainAliasCreated") -def on_relay_domain_alias_created(user, rdomainalias): - create_user_and_use_policy( - "@{0}".format(rdomainalias.name), - "@{0}".format(rdomainalias.target.name) - ) - - -@events.observe("RelayDomainAliasDeleted") -def on_relay_domain_alias_deleted(rdomainalias): - delete_user("@{0}".format(rdomainalias.name)) diff --git a/modoboa/extensions/postfix_relay_domains/app_settings.py b/modoboa/extensions/postfix_relay_domains/app_settings.py deleted file mode 100644 index 80cfc6a4d..000000000 --- a/modoboa/extensions/postfix_relay_domains/app_settings.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Relay domains settings definition. -""" - -from django import forms -from django.utils.translation import ugettext_lazy - -from modoboa.lib import parameters -from modoboa.lib.form_utils import SeparatorField - - -class AdminParametersForm(parameters.AdminParametersForm): - - """Admin parameters definition.""" - - app = "postfix_relay_domains" - - general_sep = SeparatorField(label=ugettext_lazy("General")) - - master_cf_path = forms.CharField( - label=ugettext_lazy("Postfix's master.cf path"), - initial="/etc/postfix/master.cf", - help_text=ugettext_lazy('Path to the master.cf configuration file'), - widget=forms.TextInput(attrs={"class": "form-control"}) - ) diff --git a/modoboa/extensions/postfix_relay_domains/factories.py b/modoboa/extensions/postfix_relay_domains/factories.py deleted file mode 100644 index 74400e0fb..000000000 --- a/modoboa/extensions/postfix_relay_domains/factories.py +++ /dev/null @@ -1,37 +0,0 @@ -import factory -from modoboa.core.factories import PermissionFactory -from . import models - - -class ServiceFactory(factory.DjangoModelFactory): - - """Factory to create Service instances.""" - - class Meta: - model = models.Service - django_get_or_create = ('name', ) - - name = 'dummy' - - -class RelayDomainFactory(PermissionFactory): - - """Factory to create RelayDomain instances.""" - - class Meta: - model = models.RelayDomain - - target_host = 'external.host.tld' - enabled = True - verify_recipients = True - service = factory.SubFactory(ServiceFactory) - - -class RelayDomainAliasFactory(PermissionFactory): - - """Factory to create RelayDomainAliases instances.""" - - class Meta: - model = models.RelayDomainAlias - - enabled = True diff --git a/modoboa/extensions/postfix_relay_domains/forms.py b/modoboa/extensions/postfix_relay_domains/forms.py deleted file mode 100644 index ddafe295d..000000000 --- a/modoboa/extensions/postfix_relay_domains/forms.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Postfix relay domains extension forms. -""" -from django import forms -from django.core.urlresolvers import reverse -from django.http import QueryDict -from django.utils.translation import ugettext as _, ugettext_lazy - -from .models import RelayDomain, RelayDomainAlias -from modoboa.extensions.admin.models import Domain, DomainAlias -from modoboa.lib import events -from modoboa.lib.form_utils import DynamicForm, DomainNameField, TabForms -from modoboa.lib.web_utils import render_to_json_response - - -class RelayDomainFormGeneral(forms.ModelForm, DynamicForm): - aliases = DomainNameField( - label=ugettext_lazy("Alias(es)"), - required=False, - help_text=ugettext_lazy( - "Alias(es) of this relay domain. Indicate only one name per input" - ", press ENTER to add a new input." - ) - ) - - class Meta: - model = RelayDomain - exclude = ['dates'] - widgets = { - "service": forms.Select(attrs={"class": "form-control"}) - } - - def __init__(self, *args, **kwargs): - self.oldname = None - if "instance" in kwargs: - self.oldname = kwargs["instance"].name - super(RelayDomainFormGeneral, self).__init__(*args, **kwargs) - self.field_widths = { - "service": 3 - } - if args and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "aliases", DomainNameField) - elif 'instance' in kwargs: - rd = kwargs['instance'] - for pos, rdalias in enumerate(rd.relaydomainalias_set.all()): - name = "aliases_%d" % (pos + 1) - self._create_field(forms.CharField, name, rdalias.name, 3) - - def clean(self): - """Custom fields validaton. - - We want to prevent duplicate names between domains, relay - domains, domain aliases and relay domain aliases. - - The validation way is not very smart... - """ - super(RelayDomainFormGeneral, self).clean() - cleaned_data = self.cleaned_data - for dtype, label in [(Domain, _('domain')), - (DomainAlias, _('domain alias')), - (RelayDomainAlias, _('relay domain alias'))]: - try: - dtype.objects.get(name=cleaned_data['name']) - except dtype.DoesNotExist: - pass - else: - self.add_error( - "name", _("A %s with this name already exists") % label - ) - break - - for k in cleaned_data.keys(): - if not k.startswith("aliases"): - continue - if not cleaned_data[k]: - del cleaned_data[k] - continue - for dtype, name in [(RelayDomain, _('relay domain')), - (DomainAlias, _('domain alias')), - (Domain, _('domain'))]: - try: - dtype.objects.get(name=cleaned_data[k]) - except dtype.DoesNotExist: - pass - else: - self.add_error( - k, _("A %s with this name already exists") % name) - break - - return cleaned_data - - def save(self, user, commit=True, rdomalias_post_create=False): - """Custom save method. - - As relay domain aliases are defined using the same form as - relay domains, we need to save them manually. - - :param ``User`` user: connected user - """ - rd = super(RelayDomainFormGeneral, self).save(commit=False) - if commit: - rd.oldname = self.oldname - rd.save() - aliases = [] - for k, v in self.cleaned_data.iteritems(): - if not k.startswith("aliases"): - continue - if v in ["", None]: - continue - aliases.append(v) - for rdalias in rd.relaydomainalias_set.all(): - if rdalias.name not in aliases: - rdalias.delete() - else: - aliases.remove(rdalias.name) - if aliases: - events.raiseEvent( - "CanCreate", user, "relay_domain_aliases", len(aliases) - ) - for alias in aliases: - try: - rd.relaydomainalias_set.get(name=alias) - except RelayDomainAlias.DoesNotExist: - pass - else: - continue - al = RelayDomainAlias( - name=alias, target=rd, enabled=rd.enabled - ) - al.save(creator=user) \ - if rdomalias_post_create else al.save() - return rd - - -class RelayDomainForm(TabForms): - - """Specific edition form for relay domains. - - We use a *tabs* compatible form because extensions can add their - own tab. (ex: amavis) - """ - - def __init__(self, request, *args, **kwargs): - self.user = request.user - self.forms = [] - if self.user.has_perm("postfix_relay_domains.change_relaydomain"): - self.forms.append({ - 'id': 'general', 'title': _("General"), - 'formtpl': 'postfix_relay_domains/relaydomain_form.html', - 'cls': RelayDomainFormGeneral, 'mandatory': True - }) - - cbargs = [self.user] - if "instances" in kwargs: - cbargs += [kwargs["instances"]["general"]] - self.forms += events.raiseQueryEvent("ExtraRelayDomainForm", *cbargs) - super(RelayDomainForm, self).__init__(request, *args, **kwargs) - - def extra_context(self, context): - """Additional content. - """ - rdom = self.instances["general"] - context.update({ - 'action': reverse( - "postfix_relay_domains:relaydomain_change", args=[rdom.id]), - 'formid': 'rdomform', - 'title': rdom.name - }) - - def save(self): - """Custom save method - - As forms interact with each other, it is easier to make custom - code to save them. - """ - self.forms[0]['instance'].save( - self.request.user, rdomalias_post_create=True - ) - for f in self.forms[1:]: - f["instance"].save(self.request.user) - - def done(self): - events.raiseEvent('RelayDomainModified', self.instances["general"]) - return render_to_json_response(_('Relay domain modified')) diff --git a/modoboa/extensions/postfix_relay_domains/general_callbacks.py b/modoboa/extensions/postfix_relay_domains/general_callbacks.py deleted file mode 100644 index 2dd319956..000000000 --- a/modoboa/extensions/postfix_relay_domains/general_callbacks.py +++ /dev/null @@ -1,154 +0,0 @@ -"""General event callbacks.""" - -from django.conf import settings -from django.core.urlresolvers import reverse -from django.db.models import Q -from django.template import Template, Context -from django.utils.translation import ugettext_lazy - -from modoboa.core.extensions import exts_pool -from modoboa.lib import events -from .models import RelayDomain, RelayDomainAlias - -PERMISSIONS = { - "Resellers": [ - ("postfix_relay_domains", "relaydomain", "add_relaydomain"), - ("postfix_relay_domains", "relaydomain", "change_relaydomain"), - ("postfix_relay_domains", "relaydomain", "delete_relaydomain"), - ("postfix_relay_domains", "relaydomainalias", "add_relaydomainalias"), - ("postfix_relay_domains", "relaydomainalias", - "change_relaydomainalias"), - ("postfix_relay_domains", "relaydomainalias", - "delete_relaydomainalias"), - ("postfix_relay_domains", "service", "add_service"), - ("postfix_relay_domains", "service", "change_service"), - ("postfix_relay_domains", "service", "delete_service") - ] -} - - -@events.observe("GetExtraRolePermissions") -def extra_permissions(rolename): - """Return extra permissions for Resellers.""" - if not exts_pool.is_extension_installed("modoboa.extensions.limits"): - return [] - return PERMISSIONS.get(rolename, []) - - -@events.observe('GetStaticContent') -def static_content(caller, st_type, user): - if caller != 'domains' or st_type != 'js': - return [] - - t = Template(""" - -""") - return [t.render(Context({'STATIC_URL': settings.STATIC_URL}))] - - -@events.observe('ExtraDomainFilters') -def extra_domain_filters(): - return ['srvfilter'] - - -@events.observe('ExtraDomainMenuEntries') -def extra_domain_menu_entries(user): - return [{ - "name": "newrelaydomain", - "label": ugettext_lazy("Add relay domain"), - "img": "fa fa-plus", - "modal": True, - "modalcb": "rdomain.domainform_cb", - "url": reverse("postfix_relay_domains:relaydomain_add") - }] - - -@events.observe('ExtraDomainEntries') -def extra_domain_entries(user, domfilter, searchquery, **extrafilters): - if domfilter is not None and domfilter and domfilter != 'relaydomain': - return [] - relay_domains = RelayDomain.objects.get_for_admin(user) - if searchquery is not None: - q = Q(name__contains=searchquery) - q |= Q(relaydomainalias__name__contains=searchquery) - relay_domains = relay_domains.filter(q).distinct() - if 'srvfilter' in extrafilters and extrafilters['srvfilter']: - relay_domains = relay_domains.filter( - Q(service__name=extrafilters['srvfilter']) - ) - return relay_domains - - -@events.observe('GetDomainModifyLink') -def rdomain_modify_link(domain): - if not isinstance(domain, RelayDomain): - return {} - return { - 'url': reverse( - "postfix_relay_domains:relaydomain_change", - args=[domain.id] - ), - 'modalcb': 'rdomain.editdomain_form_cb' - } - - -@events.observe('GetDomainActions') -def rdomain_actions(user, domain): - if not isinstance(domain, RelayDomain): - return [] - if not user.has_perm("postfix_relay_domains.delete_relaydomain"): - return [] - return [{ - "name": "delrelaydomain", - "url": reverse( - "postfix_relay_domains:relaydomain_delete", - args=[domain.id] - ), - "title": ugettext_lazy("Delete %s?" % domain.name), - "img": "fa fa-trash" - }] - - -@events.observe('CheckDomainName') -def check_domain_name(): - return [ - (RelayDomain, ugettext_lazy('relay domain')), - (RelayDomainAlias, ugettext_lazy('relay domain alias')) - ] - - -@events.observe('GetExtraLimitTemplates') -def extra_limit_templates(): - return [ - ('relay_domains_limit', ugettext_lazy('Relay domains'), - ugettext_lazy('Maximum number of relay domains this user can create'), - 'Resellers'), - ("relay_domain_aliases_limit", ugettext_lazy("Relay domain aliases"), - ugettext_lazy( - 'Maximum number of relay domain aliases this user can create'), - 'Resellers'), - ] - - -@events.observe('ExtraDomainImportHelp') -def extra_domain_import_help(): - return [ugettext_lazy(""" -
  • relaydomain; name; target host; service; enabled; verify recipients
  • -
  • relaydomainalias; name; target; enabled
  • -""")] - - -@events.observe('ImportObject') -def get_import_func(objtype): - from .lib import import_relaydomain, import_relaydomainalias - - if objtype == 'relaydomain': - return [import_relaydomain] - if objtype == 'relaydomainalias': - return [import_relaydomainalias] - return [] diff --git a/modoboa/extensions/postfix_relay_domains/lib.py b/modoboa/extensions/postfix_relay_domains/lib.py deleted file mode 100644 index a5c4764ec..000000000 --- a/modoboa/extensions/postfix_relay_domains/lib.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.db import transaction -from .models import RelayDomain, RelayDomainAlias - - -@transaction.commit_on_success -def import_relaydomain(user, row, formopts): - """Specific code for relay domains import""" - RelayDomain().from_csv(user, row) - - -@transaction.commit_on_success -def import_relaydomainalias(user, row, formopts): - """Specific code for relay domain aliases import""" - RelayDomainAlias().from_csv(user, row) diff --git a/modoboa/extensions/postfix_relay_domains/limits_callbacks.py b/modoboa/extensions/postfix_relay_domains/limits_callbacks.py deleted file mode 100644 index 7a7b362cf..000000000 --- a/modoboa/extensions/postfix_relay_domains/limits_callbacks.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Callbacks related to limits extension.""" - -from django.utils.translation import ugettext_lazy - -from modoboa.extensions.limits.lib import inc_limit_usage, dec_limit_usage -from modoboa.extensions.limits.models import LimitsPool, Limit -from modoboa.lib import events -from modoboa.lib.permissions import get_object_owner -from .models import RelayDomainAlias - - -def create_new_limits(): - """Create relay domains limits.""" - - new_limits = ["relay_domains_limit", "relay_domain_aliases_limit"] - for pool in LimitsPool.objects.all(): - for lname in new_limits: - Limit.objects.get_or_create(name=lname, pool=pool, maxvalue=0) - - -@events.observe("InitialDataLoaded") -def load_limits_dependant_data(name): - """Complete existing pools with new limits.""" - if name != "limits": - return - create_new_limits() - - -@events.observe('GetExtraParameters') -def extra_parameters(app, level): - from django import forms - - if app != 'limits' or level != 'A': - return {} - return { - 'deflt_relay_domains_limit': forms.IntegerField( - label=ugettext_lazy("Relay domains"), - initial=0, - help_text=ugettext_lazy( - "Maximum number of allowed relay domains for a new " - "administrator" - ), - widget=forms.widgets.TextInput( - attrs={ - "class": "col-md-1 form-control"}) - ), - 'deflt_relay_domain_aliases_limit': forms.IntegerField( - label=ugettext_lazy("Relay domain aliases"), - initial=0, - help_text=ugettext_lazy( - "Maximum number of allowed relay domain aliases for a new " - "administrator" - ), - widget=forms.widgets.TextInput( - attrs={ - "class": "col-md-1 form-control"}) - ) - } - - -@events.observe('RelayDomainCreated') -def inc_relaydomains_count(user, rdomain): - inc_limit_usage(user, 'relay_domains_limit') - - -@events.observe('RelayDomainDeleted') -def dec_relaydomains_count(rdomain): - owner = get_object_owner(rdomain) - dec_limit_usage(owner, 'relay_domains_limit') - for rdomalias in rdomain.relaydomainalias_set.all(): - dec_rdomaliases_count(rdomalias) - - -@events.observe('RelayDomainAliasCreated') -def inc_rdomaliases_count(user, rdomalias): - inc_limit_usage(user, 'relay_domain_aliases_limit') - - -@events.observe('RelayDomainAliasDeleted') -def dec_rdomaliases_count(rdomainaliases): - if isinstance(rdomainaliases, RelayDomainAlias): - rdomainaliases = [rdomainaliases] - for rdomainalias in rdomainaliases: - owner = get_object_owner(rdomainalias) - dec_limit_usage(owner, 'relay_domain_aliases_limit') diff --git a/modoboa/extensions/postfix_relay_domains/migrations/0001_initial.py b/modoboa/extensions/postfix_relay_domains/migrations/0001_initial.py deleted file mode 100644 index 311a5dc0d..000000000 --- a/modoboa/extensions/postfix_relay_domains/migrations/0001_initial.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('admin', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='RelayDomain', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The domain name', unique=True, max_length=100, verbose_name='name')), - ('target_host', models.CharField(help_text='Remote destination of this domain', max_length=255, verbose_name='target host')), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this domain', verbose_name='enabled')), - ('verify_recipients', models.BooleanField(default=False, help_text='Check for valid recipients', verbose_name='verify recipients')), - ('dates', models.ForeignKey(to='admin.ObjectDates')), - ], - options={ - 'ordering': ['name'], - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='RelayDomainAlias', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The alias name', unique=True, max_length=100, verbose_name='name')), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')), - ('dates', models.ForeignKey(to='admin.ObjectDates')), - ('target', models.ForeignKey(verbose_name='target', to='postfix_relay_domains.RelayDomain', help_text='The relay domain this alias points to')), - ], - options={ - 'abstract': False, - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Service', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The service name', unique=True, max_length=100, verbose_name='name')), - ], - options={ - }, - bases=(models.Model,), - ), - migrations.AddField( - model_name='relaydomain', - name='service', - field=models.ForeignKey(to='postfix_relay_domains.Service'), - preserve_default=True, - ), - ] diff --git a/modoboa/extensions/postfix_relay_domains/migrations/__init__.py b/modoboa/extensions/postfix_relay_domains/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/postfix_relay_domains/models.py b/modoboa/extensions/postfix_relay_domains/models.py deleted file mode 100644 index 17807373e..000000000 --- a/modoboa/extensions/postfix_relay_domains/models.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Relay domain related models.""" - -from django.contrib.contenttypes import generic -from django.db import models -from django.db.models.manager import Manager -from django.utils.translation import ugettext as _, ugettext_lazy - -import reversion - -from modoboa.core.models import ObjectAccess -from modoboa.extensions.admin.models import AdminObject -from modoboa.lib import parameters -from modoboa.lib.exceptions import BadRequest, NotFound - - -class RelayDomainManager(Manager): - - def get_for_admin(self, admin): - """Return the relay domains belonging to this admin. - - The result is a ``QuerySet`` object, so this function can be used - to fill ``ModelChoiceField`` objects. - """ - if admin.is_superuser: - return self.get_query_set() - return self.get_query_set().filter(owners__user=admin) - - -class ServiceManager(Manager): - - def load_from_master_cf(self): - """Load services from the master.cf file. - - Parse the configuration file to update the service table. New - entries are saved and outdated ones (ie. present in the - database but not in the file) are removed. - """ - with open(parameters.get_admin('MASTER_CF_PATH')) as fp: - content = fp.read() - services = [] - for line in content.split('\n'): - if not line or line.startswith('#'): - continue - parts = line.strip().split() - if len(parts) != 8: - continue - if parts[7] != 'smtp': - continue - srv, created = self.get_or_create(name=parts[0]) - services.append(parts[0]) - to_delete = [] - for service in self.all(): - if service.name not in services: - to_delete.append(service.name) - Service.objects.filter(name__in=to_delete).delete() - - -class Service(models.Model): - - """Postfix service. - """ - name = models.CharField( - ugettext_lazy('name'), max_length=100, unique=True, - help_text=ugettext_lazy('The service name') - ) - - objects = ServiceManager() - - def __str__(self): - return self.name - - -class RelayDomain(AdminObject): - - """Relay domain. - - A relay domain differs from a usual domaine because its final - destination is not reached yet. It must be accepted by the MTA but - it will then be transfered to another one. - """ - name = models.CharField( - ugettext_lazy('name'), max_length=100, unique=True, - help_text=ugettext_lazy('The domain name') - ) - target_host = models.CharField( - ugettext_lazy('target host'), max_length=255, - help_text=ugettext_lazy('Remote destination of this domain') - ) - service = models.ForeignKey(Service) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy('Check to activate this domain'), - default=True - ) - verify_recipients = models.BooleanField( - ugettext_lazy('verify recipients'), - help_text=ugettext_lazy('Check for valid recipients'), - default=False - ) - - owners = generic.GenericRelation(ObjectAccess) - - objects = RelayDomainManager() - - class Meta: - ordering = ['name'] - - @property - def tags(self): - return [ - {"name": "relaydomain", "label": _("Relay Domain"), "type": "dom"}, - {"name": self.service.name, "label": "%s:" % self.service.name, - "type": "srv", "color": "info"} - ] - - @property - def aliases(self): - return self.relaydomainalias_set - - def __str__(self): - return self.name - - def to_csv(self, csvwriter): - """Export this relay domain to CSV. - - :param csvwriter: - """ - csvwriter.writerow( - ["relaydomain", self.name, self.target_host, - self.service.name, self.enabled, self.verify_recipients] - ) - for rdalias in self.relaydomainalias_set.all(): - rdalias.to_csv(csvwriter) - - def from_csv(self, user, row): - """Import a relay domain from CSV. - - :param user: user importing the relay domain - :param str row: relay domain definition - """ - if len(row) != 6: - raise BadRequest(_("Invalid line")) - self.name = row[1].strip() - self.target_host = row[2].strip() - self.service, created = Service.objects.get_or_create( - name=row[3].strip()) - self.enabled = (row[4].strip() == 'True') - self.verify_recipients = (row[5].strip() == 'True') - self.save(creator=user) - - def post_create(self, creator): - """Post creation actions. - - :param ``User`` creator: user whos created this relay domain - """ - super(RelayDomain, self).post_create(creator) - for rdomalias in self.relaydomainalias_set.all(): - rdomalias.post_create(creator) - -reversion.register(RelayDomain) - - -class RelayDomainAlias(AdminObject): - - """Relay domain alias. - - """ - name = models.CharField( - ugettext_lazy("name"), max_length=100, unique=True, - help_text=ugettext_lazy("The alias name") - ) - target = models.ForeignKey( - RelayDomain, verbose_name=ugettext_lazy('target'), - help_text=ugettext_lazy("The relay domain this alias points to") - ) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this alias"), - default=True - ) - - def __str__(self): - return self.name - - def to_csv(self, csvwriter): - """Export this relay domain alias to CSV. - - :param csvwriter: - """ - csvwriter.writerow( - ['relaydomainalias', self.name, self.target.name, self.enabled] - ) - - def from_csv(self, user, row): - """Import a relay domain alias from CSV. - - :param user: user importing the relay domain alias - :param str row: relay domain alias definition - """ - if len(row) != 4: - raise BadRequest(_("Invalid line")) - self.name = row[1].strip() - try: - self.target = RelayDomain.objects.get(name=row[2].strip()) - except RelayDomain.DoesNotExist: - raise NotFound(_("Relay domain %s does not exist" % row[2].strip())) - self.enabled = (row[3].strip() == 'True') - self.save(creator=user) - -reversion.register(RelayDomainAlias) diff --git a/modoboa/extensions/postfix_relay_domains/modo_extension.py b/modoboa/extensions/postfix_relay_domains/modo_extension.py deleted file mode 100644 index 4694f801a..000000000 --- a/modoboa/extensions/postfix_relay_domains/modo_extension.py +++ /dev/null @@ -1,55 +0,0 @@ -# coding: utf-8 - -"""Declare postfix_relay_domains extension and register it.""" - -from django.utils.translation import ugettext_lazy - -from modoboa.core.extensions import ModoExtension, exts_pool -from modoboa.lib import events, parameters - - -EXTENSION_EVENTS = [ - "RelayDomainCreated", - "RelayDomainDeleted", - "RelayDomainModified", - "RelayDomainAliasCreated", - "RelayDomainAliasDeleted", - "ExtraRelayDomainForm", - "FillRelayDomainInstances" -] - - -class PostfixRelayDomains(ModoExtension): - - """Extension declaration.""" - - name = "postfix_relay_domains" - label = "Postfix relay domains" - version = "1.0" - description = ugettext_lazy("Relay domains support for Postfix") - - def load(self): - from .app_settings import AdminParametersForm - - parameters.register( - AdminParametersForm, ugettext_lazy("Relay domains") - ) - events.declare(EXTENSION_EVENTS) - from modoboa.extensions.postfix_relay_domains import general_callbacks - if exts_pool.is_extension_installed("modoboa.extensions.limits"): - import limits_callbacks - if exts_pool.is_extension_installed("modoboa.extensions.amavis"): - import amavis_callbacks - - def load_initial_data(self): - """Create extension data.""" - from .models import Service - for service_name in ['relay', 'smtp']: - Service.objects.get_or_create(name=service_name) - - if not exts_pool.is_extension_installed("modoboa.extensions.limits"): - return - from .limits_callbacks import create_new_limits - create_new_limits() - -exts_pool.register_extension(PostfixRelayDomains) diff --git a/modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js b/modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js deleted file mode 100644 index dddc99e8d..000000000 --- a/modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @module relay_domains - * @desc Utility class to manage relay domains. - */ - -var RelayDomains = function(options) { - this.initialize(options); -}; - -RelayDomains.prototype = { - constructor: RelayDomains, - - defaults: { - }, - - initialize: function(options) { - this.options = $.extend({}, this.defaults, options); - admin.register_tag_handler("srv", this.srv_tag_handler); - $(document).bind('domain_listing_refresh', this.listing_refresh_cb); - }, - - listing_refresh_cb: function() { - $("a[name=delrelaydomain]").confirm({ - question: function() { return this.$element.attr('title'); }, - method: "DELETE", - success_cb: $.proxy(admin.reload_listing, admin) - }); - }, - - launch_scan: function(e) { - var $link = get_target(e); - - e.preventDefault(); - $.ajax({ - type: "POST", - url: $link.attr("href"), - global: false - }).done(function(data) { - var $widget = $('#id_service'); - - $widget.empty(); - $.each(data, function(label, id) { - $widget.append($('