Repository: Mailu/Mailu

Issue:
Admin interface errors with leading whitespace in domain name of new domain view
Thank you for opening an issue with Mailu. Please understand that issues are meant for bugs and enhancement-requests.
For **user-support questions**, reach out to us  on [matrix](https://matrix.to/#/#mailu:tedomum.net).

To be able to help you best, we need some more information.

## Before you open your issue
- [x] Check if no issue or pull-request for this already exists.
- [x] Check [documentation](https://mailu.io/master/) and [FAQ](https://mailu.io/master/faq.html). (Tip, use the search function on the documentation page)
- [x] You understand `Mailu` is made by volunteers in their **free time** — be conscise, civil and accept that delays can occur.
- [x] The title of the issue should be short and simple. It should contain specific terms related to the actual issue. Be specific while writing the title.

## Environment & Versions
### Environment
 - [x] docker-compose
 - [ ] kubernetes
 - [ ] docker swarm

### Versions
1.7

## Description
The "New Domain" dialog view (`/admin/ui/domain/create`) crashes when the "Domain Name" field contains leading whitespace

## Replication Steps
- Open the Admin Interface
- Navigate to "Mail Domains" using the Sidenav
- Click on "New Domain" in the top right corner
- Write a string with a leading space like "` test`" in the "Domain Name" field
- Click "Save"
- The page crashes

## Expected behaviour
The input field could self validate the input and indicate that its invalid or the input string for domains should get trimmed.

## Logs
````markdown
```
mail-admin  | [2021-05-26 15:00:32,624] ERROR in app: Exception on /ui/domain/create [POST]
mail-admin  | Traceback (most recent call last):
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1179, in _execute_context
mail-admin  |     context = constructor(dialect, self, conn, *args)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/default.py", line 745, in _init_compiled
mail-admin  |     for key in compiled_params
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/default.py", line 745, in <genexpr>
mail-admin  |     for key in compiled_params
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/sql/type_api.py", line 1201, in process
mail-admin  |     return process_param(value, dialect)
mail-admin  |   File "/app/mailu/models.py", line 30, in process_bind_param
mail-admin  |     return idna.encode(value).decode("ascii").lower()
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 358, in encode
mail-admin  |     s = alabel(label)
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 270, in alabel
mail-admin  |     ulabel(label)
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 304, in ulabel
mail-admin  |     check_label(label)
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 261, in check_label
mail-admin  |     raise InvalidCodepoint('Codepoint {0} at position {1} of {2} not allowed'.format(_unot(cp_value), pos+1, repr(label)))
mail-admin  | idna.core.InvalidCodepoint: Codepoint U+0020 at position 1 of ' test ' not allowed
mail-admin  | 
mail-admin  | The above exception was the direct cause of the following exception:
mail-admin  | 
mail-admin  | Traceback (most recent call last):
mail-admin  |   File "/usr/lib/python3.7/site-packages/flask/app.py", line 2292, in wsgi_app
mail-admin  |     response = self.full_dispatch_request()
mail-admin  |   File "/usr/lib/python3.7/site-packages/flask/app.py", line 1815, in full_dispatch_request
mail-admin  |     rv = self.handle_user_exception(e)
mail-admin  |   File "/usr/lib/python3.7/site-packages/flask/app.py", line 1718, in handle_user_exception
mail-admin  |     reraise(exc_type, exc_value, tb)
mail-admin  |   File "/usr/lib/python3.7/site-packages/flask/_compat.py", line 35, in reraise
mail-admin  |     raise value
mail-admin  |   File "/usr/lib/python3.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
mail-admin  |     rv = self.dispatch_request()
mail-admin  |   File "/usr/lib/python3.7/site-packages/flask/app.py", line 1799, in dispatch_request
mail-admin  |     return self.view_functions[rule.endpoint](**req.view_args)
mail-admin  |   File "/usr/lib/python3.7/site-packages/flask_login/utils.py", line 261, in decorated_view
mail-admin  |     return func(*args, **kwargs)
mail-admin  |   File "/app/mailu/ui/access.py", line 36, in wrapper
mail-admin  |     return callback(function, args, kwargs, (), {})
mail-admin  |   File "/app/mailu/ui/access.py", line 19, in callback
mail-admin  |     return function(*args, **kwargs)
mail-admin  |   File "/app/mailu/ui/views/domains.py", line 22, in domain_create
mail-admin  |     conflicting_domain = models.Domain.query.get(form.name.data)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/orm/query.py", line 959, in get
mail-admin  |     return self._get_impl(ident, loading.load_on_pk_identity)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/orm/query.py", line 1069, in _get_impl
mail-admin  |     return db_load_fn(self, primary_key_identity)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/orm/loading.py", line 282, in load_on_pk_identity
mail-admin  |     return q.one()
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/orm/query.py", line 3275, in one
mail-admin  |     ret = self.one_or_none()
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/orm/query.py", line 3244, in one_or_none
mail-admin  |     ret = list(self)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/orm/query.py", line 3317, in __iter__
mail-admin  |     return self._execute_and_instances(context)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/orm/query.py", line 3342, in _execute_and_instances
mail-admin  |     result = conn.execute(querycontext.statement, self._params)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 988, in execute
mail-admin  |     return meth(self, multiparams, params)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/sql/elements.py", line 287, in _execute_on_connection
mail-admin  |     return connection._execute_clauseelement(self, multiparams, params)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1107, in _execute_clauseelement
mail-admin  |     distilled_params,
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context
mail-admin  |     e, util.text_type(statement), parameters, None, None
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1466, in _handle_dbapi_exception
mail-admin  |     util.raise_from_cause(sqlalchemy_exception, exc_info)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/util/compat.py", line 383, in raise_from_cause
mail-admin  |     reraise(type(exception), exception, tb=exc_tb, cause=cause)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/util/compat.py", line 128, in reraise
mail-admin  |     raise value.with_traceback(tb)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/base.py", line 1179, in _execute_context
mail-admin  |     context = constructor(dialect, self, conn, *args)
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/default.py", line 745, in _init_compiled
mail-admin  |     for key in compiled_params
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/engine/default.py", line 745, in <genexpr>
mail-admin  |     for key in compiled_params
mail-admin  |   File "/usr/lib/python3.7/site-packages/sqlalchemy/sql/type_api.py", line 1201, in process
mail-admin  |     return process_param(value, dialect)
mail-admin  |   File "/app/mailu/models.py", line 30, in process_bind_param
mail-admin  |     return idna.encode(value).decode("ascii").lower()
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 358, in encode
mail-admin  |     s = alabel(label)
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 270, in alabel
mail-admin  |     ulabel(label)
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 304, in ulabel
mail-admin  |     check_label(label)
mail-admin  |   File "/usr/lib/python3.7/site-packages/idna/core.py", line 261, in check_label
mail-admin  |     raise InvalidCodepoint('Codepoint {0} at position {1} of {2} not allowed'.format(_unot(cp_value), pos+1, repr(label)))
mail-admin  | sqlalchemy.exc.StatementError: (idna.core.InvalidCodepoint) Codepoint U+0020 at position 1 of ' test ' not allowed
mail-admin  | [SQL: SELECT domain.created_at AS domain_created_at, domain.updated_at AS domain_updated_at, domain.comment AS domain_comment, domain.name AS domain_name, domain.max_users AS domain_max_users, domain.max_aliases AS domain_max_aliases, domain.max_quota_bytes AS domain_max_quota_bytes, domain.signup_enabled AS domain_signup_enabled 
mail-admin  | FROM domain 
mail-admin  | WHERE domain.name = %(param_1)s]
mail-admin  | [parameters: [{'%(140672597756432 param)s': ' test '}]]
```
````



Discussion:
Thank you for reporting this bug. You would indeed expect the input to be validated or the whitespace to be stripped.

Resolving patch:
diff --git a/core/admin/mailu/ui/views/domains.py b/core/admin/mailu/ui/views/domains.py
--- a/core/admin/mailu/ui/views/domains.py
+++ b/core/admin/mailu/ui/views/domains.py
@@ -2,6 +2,7 @@
 from mailu.ui import ui, forms, access
 from flask import current_app as app
 
+import validators
 import flask
 import flask_login
 import wtforms_components
@@ -18,18 +19,21 @@ def domain_list():
 def domain_create():
     form = forms.DomainForm()
     if form.validate_on_submit():
-        conflicting_domain = models.Domain.query.get(form.name.data)
-        conflicting_alternative = models.Alternative.query.get(form.name.data)
-        conflicting_relay = models.Relay.query.get(form.name.data)
-        if conflicting_domain or conflicting_alternative or conflicting_relay:
-            flask.flash('Domain %s is already used' % form.name.data, 'error')
+        if validators.domain(form.name.data):
+            conflicting_domain = models.Domain.query.get(form.name.data)
+            conflicting_alternative = models.Alternative.query.get(form.name.data)
+            conflicting_relay = models.Relay.query.get(form.name.data)
+            if conflicting_domain or conflicting_alternative or conflicting_relay:
+                flask.flash('Domain %s is already used' % form.name.data, 'error')
+            else:
+                domain = models.Domain()
+                form.populate_obj(domain)
+                models.db.session.add(domain)
+                models.db.session.commit()
+                flask.flash('Domain %s created' % domain)
+                return flask.redirect(flask.url_for('.domain_list'))
         else:
-            domain = models.Domain()
-            form.populate_obj(domain)
-            models.db.session.add(domain)
-            models.db.session.commit()
-            flask.flash('Domain %s created' % domain)
-            return flask.redirect(flask.url_for('.domain_list'))
+            flask.flash('Domain %s is invalid' % form.name.data, 'error')
     return flask.render_template('domain/create.html', form=form)
 
 


Test patch: