Unique emails and added can_receive

This commit is contained in:
Finn Stutzenstein 2022-03-06 09:34:23 +01:00
parent ba580483b4
commit aff7bf3b48
22 changed files with 174 additions and 37 deletions

View File

@ -1,7 +1,12 @@
FROM python:3.8.1 FROM python:3.8.12-slim-buster
RUN apt-get -y update && apt-get -y upgrade && \ RUN apt-get -y update && apt-get -y upgrade && \
apt-get install --no-install-recommends -y wait-for-it postgresql-client apt-get install --no-install-recommends -y wait-for-it lsb-release wget gnupg2 gcc libpq-dev libc-dev
# install psql 13 (11 is the default with debian)
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" |sudo tee /etc/apt/sources.list.d/pgdg.list
RUN apt-get update && apt-get --no-install-recommends -y install postgresql-client-13 && apt-get clean all
WORKDIR /app WORKDIR /app

View File

@ -1,7 +1,12 @@
FROM python:3.8.1 FROM python:3.8.12-slim-buster
RUN apt-get -y update && apt-get -y upgrade && \ RUN apt-get -y update && apt-get -y upgrade && \
apt-get install --no-install-recommends -y wait-for-it postgresql-client vim apt-get install --no-install-recommends -y wait-for-it lsb-release wget gnupg2 gcc libpq-dev libc-dev vim
# install psql 13 (11 is the default with debian)
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN apt-get update && apt-get --no-install-recommends -y install postgresql-client-13 && apt-get clean all
WORKDIR /app WORKDIR /app

View File

@ -14,11 +14,17 @@ run-bash: | build-dev
docker-compose -f dc.dev.yml exec -u $$(id -u $${USER}):$$(id -g $${USER}) max bash || true docker-compose -f dc.dev.yml exec -u $$(id -u $${USER}):$$(id -g $${USER}) max bash || true
docker-compose -f dc.dev.yml down docker-compose -f dc.dev.yml down
run-bash-root: | build-dev
MAX_COMMAND="sleep infinity" docker-compose -f dc.dev.yml up -d
docker-compose -f dc.dev.yml exec max "./entrypoint-dev.sh"
docker-compose -f dc.dev.yml exec max bash || true
docker-compose -f dc.dev.yml down
stop-dev: stop-dev:
docker-compose -f dc.dev.yml down docker-compose -f dc.dev.yml down
run-cleanup: | build-dev run-cleanup: | build-dev
docker run -ti -v `pwd`/max:/app/max max-dev ./cleanup.sh docker run -ti -v `pwd`/max:/app/max --entrypoint="" max-dev ./cleanup.sh
# PROD # PROD

View File

@ -12,6 +12,7 @@ class Alias(NoteLinesMixin):
email: str email: str
destination_user_id: int destination_user_id: int
enabled: bool enabled: bool
can_receive: bool
note: str note: str
@ -35,8 +36,9 @@ def get_aliases(cur, user_id):
def get_alias_with_user_by_id(cur, alias_id): def get_alias_with_user_by_id(cur, alias_id):
cur.execute( cur.execute(
"""\ """\
SELECT a.id, a.source_email_id, ea.email, a.destination_user_id, a.enabled, a.note, SELECT a.id, a.source_email_id, ea.email, a.destination_user_id, a.enabled,
u.id, u.email_id, eu.email, u.passwordhash, u.enabled, u.is_admin, u.note a.can_receive, a.note,
u.id, u.email_id, eu.email, u.passwordhash, u.enabled, u.can_receive, u.is_admin, u.note
FROM aliases a FROM aliases a
INNER JOIN emails ea ON a.source_email_id=ea.id INNER JOIN emails ea ON a.source_email_id=ea.id
INNER JOIN users u ON a.destination_user_id=u.id INNER JOIN users u ON a.destination_user_id=u.id
@ -47,23 +49,23 @@ def get_alias_with_user_by_id(cur, alias_id):
row = cur.fetchone() row = cur.fetchone()
if not row: if not row:
return None return None
return Alias(*row[:6]), User(*row[6:]) return Alias(*row[:7]), User(*row[7:])
@with_cursor @with_cursor
def create_alias(cur, email, user, enabled, note): def create_alias(cur, email, user, enabled, can_receive, note):
email_id = create_email(cur, email) email_id = create_email(cur, email)
cur.execute( cur.execute(
""" """
INSERT INTO aliases (source_email_id, destination_user_id, enabled, note) INSERT INTO aliases (source_email_id, destination_user_id, enabled, can_receive, note)
VALUES (%s, %s, %s, %s)""", VALUES (%s, %s, %s, %s, %s)""",
[email_id, user.id, enabled, note], [email_id, user.id, enabled, can_receive, note],
) )
@with_cursor @with_cursor
def update_alias(cur, id, **fields): def update_alias(cur, id, **fields):
"""updates enabled and note""" """updates enabled, can_receive and note"""
cur.execute( cur.execute(
"UPDATE aliases SET " "UPDATE aliases SET "
+ ", ".join(f"{field}=%s" for field in fields.keys()) + ", ".join(f"{field}=%s" for field in fields.keys())

View File

@ -6,15 +6,17 @@ class EmailAlreadyExists(Exception):
class NoteLinesMixin: class NoteLinesMixin:
def note_lines(self): def note_lines(self):
return [line.strip() for line in self.note.split("\n")] if not self.note.strip():
return []
return [line.strip() for line in self.note.strip().split("\n")]
def create_email(cur, email): def create_email(cur, email):
cur.execute("SELECT EXISTS (SELECT 1 FROM emails WHERE email=%s)", [email]) cur.execute("SELECT EXISTS (SELECT 1 FROM emails WHERE email=lower(%s))", [email])
email_exists = cur.fetchone()[0] email_exists = cur.fetchone()[0]
if email_exists: if email_exists:
cur.execute("SELECT id FROM emails WHERE email=%s", [email]) cur.execute("SELECT id FROM emails WHERE email=lower(%s)", [email])
email_id = cur.fetchone()[0] email_id = cur.fetchone()[0]
# Get user? # Get user?
@ -35,6 +37,8 @@ def create_email(cur, email):
raise EmailAlreadyExists("Used as an user.") raise EmailAlreadyExists("Used as an user.")
else: else:
cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email]) cur.execute(
"INSERT INTO emails (email) VALUES (lower(%s)) RETURNING id", [email]
)
email_id = cur.fetchone()[0] email_id = cur.fetchone()[0]
return email_id return email_id

View File

@ -7,20 +7,26 @@ CREATE TABLE IF NOT EXISTS emails (
email varchar(255) NOT NULL UNIQUE email varchar(255) NOT NULL UNIQUE
); );
-- Make emails unique by lower-case comparison
CREATE UNIQUE INDEX email_unique_idx on emails (LOWER(email));
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id serial PRIMARY KEY, id serial PRIMARY KEY,
email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL UNIQUE, email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL UNIQUE,
passwordhash varchar(255) NOT NULL, passwordhash varchar(255) NOT NULL,
enabled boolean NOT NULL DEFAULT TRUE, enabled boolean NOT NULL DEFAULT TRUE,
can_receive boolean NOT NULL DEFAULT TRUE,
is_admin boolean NOT NULL DEFAULT FALSE, is_admin boolean NOT NULL DEFAULT FALSE,
note text NOT NULL DEFAULT '' note text NOT NULL DEFAULT ''
); );
-- source=me2@finn.st destination=me@finn.st: me2 is an alias of me
CREATE TABLE IF NOT EXISTS aliases ( CREATE TABLE IF NOT EXISTS aliases (
id serial PRIMARY KEY, id serial PRIMARY KEY,
source_email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL, source_email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL,
destination_user_id integer REFERENCES users(id) ON DELETE RESTRICT NOT NULL, destination_user_id integer REFERENCES users(id) ON DELETE RESTRICT NOT NULL,
enabled boolean NOT NULL DEFAULT TRUE, enabled boolean NOT NULL DEFAULT TRUE,
can_receive boolean NOT NULL DEFAULT TRUE,
note text, note text,
UNIQUE(source_email_id, destination_user_id) UNIQUE(source_email_id, destination_user_id)
); );

View File

@ -11,6 +11,7 @@ class User(NoteLinesMixin):
email: str email: str
passwordhash: str passwordhash: str
enabled: bool enabled: bool
can_receive: bool
is_admin: bool is_admin: bool
note: str note: str
@ -52,7 +53,7 @@ def get_users(cur):
@with_cursor @with_cursor
def update_user(cur, id, **fields): def update_user(cur, id, **fields):
"""only pwhash, enabled, is_admin, note""" """only pwhash, enabled, can_receive, is_admin, note"""
cur.execute( cur.execute(
"UPDATE users SET " "UPDATE users SET "
+ ", ".join(f"{field}=%s" for field in fields.keys()) + ", ".join(f"{field}=%s" for field in fields.keys())
@ -62,13 +63,13 @@ def update_user(cur, id, **fields):
@with_cursor @with_cursor
def create_user(cur, email, passwordhash, enabled, is_admin, note): def create_user(cur, email, passwordhash, enabled, can_receive, is_admin, note):
email_id = create_email(cur, email) email_id = create_email(cur, email)
cur.execute( cur.execute(
"""\ """\
INSERT INTO users (email_id, passwordhash, enabled, is_admin, note) INSERT INTO users (email_id, passwordhash, enabled, can_receive, is_admin, note)
VALUES (%s, %s, %s, %s, %s)""", VALUES (%s, %s, %s, %s, %s, %s)""",
[email_id, passwordhash, enabled, is_admin, note], [email_id, passwordhash, enabled, can_receive, is_admin, note],
) )
@ -92,8 +93,8 @@ def reset_or_create_user(cur, email, hash, is_admin):
user_id = user_id[0] user_id = user_id[0]
# update User # update User
cur.execute( cur.execute(
"UPDATE users SET passwordhash=%s, is_admin=%s, enabled=%s WHERE id=%s", "UPDATE users SET passwordhash=%s, is_admin=%s, enabled=%s can_receive=%s WHERE id=%s", # noqa
[hash, is_admin, True, user_id], [hash, is_admin, True, True, user_id],
) )
print( print(
f"reset password for {email}. Set admin={is_admin} and enabled the user." f"reset password for {email}. Set admin={is_admin} and enabled the user."
@ -105,8 +106,8 @@ def reset_or_create_user(cur, email, hash, is_admin):
cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email]) cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email])
email_id = cur.fetchone()[0] email_id = cur.fetchone()[0]
cur.execute( cur.execute(
"INSERT INTO users (email_id, passwordhash, enabled, is_admin, note) VALUES (%s, %s, %s, %s, %s)", # noqa "INSERT INTO users (email_id, passwordhash, enabled, can_receive, is_admin, note) VALUES (%s, %s, %s, %s, %s, %s)", # noqa
[email_id, hash, True, is_admin, "Created by commandline"], [email_id, hash, True, True, is_admin, "Created by commandline"],
) )
user_type = "admin" if is_admin else "user" user_type = "admin" if is_admin else "user"
print(f"Created {user_type} {email}") print(f"Created {user_type} {email}")

View File

@ -58,7 +58,7 @@ class ExceptionMiddleware(BaseMiddleware):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
try: try:
return self.handler(*args, **kwargs) return self.handler(*args, **kwargs)
except (NotFound, PermissionDenied) as e: except (NotFound, PermissionDenied):
notFoundView = NotFoundView(**kwargs) notFoundView = NotFoundView(**kwargs)
return notFoundView.render(), 404 return notFoundView.render(), 404

View File

@ -1,6 +1,6 @@
from max.permissions import AllowAnyAccess from max.permissions import AllowAnyAccess
from max.views import BaseTemplateGetView
from max.translations import t from max.translations import t
from max.views import BaseTemplateGetView
class NotFoundView(AllowAnyAccess, BaseTemplateGetView): class NotFoundView(AllowAnyAccess, BaseTemplateGetView):

View File

@ -19,6 +19,7 @@
"Create new user": "Neuen Nutzer erstellen", "Create new user": "Neuen Nutzer erstellen",
"Create": "Erstellen", "Create": "Erstellen",
"Enabled": "Aktiviert", "Enabled": "Aktiviert",
"Can receive emails": "Kann Emails empfangen",
"Admin": "Admin", "Admin": "Admin",
"Note": "Notiz", "Note": "Notiz",
"Delete {user}?": "{user} löschen?", "Delete {user}?": "{user} löschen?",
@ -47,6 +48,7 @@
"The domain is invalid. Must be one of: {domains}": "Die Domain ist nacht valide. Dies sind die erlaubten Domains: {domains}", "The domain is invalid. Must be one of: {domains}": "Die Domain ist nacht valide. Dies sind die erlaubten Domains: {domains}",
"Email must be given": "Die Email muss gegeben sein", "Email must be given": "Die Email muss gegeben sein",
"Enabled is not set": "Aktiviert ist nicht gesetzt", "Enabled is not set": "Aktiviert ist nicht gesetzt",
"Can receive is not set": "Kann empfangen ist nicht gesetzt",
"Note must be a string": "Die Notiz muss eine Zeichenkette sein", "Note must be a string": "Die Notiz muss eine Zeichenkette sein",
"Creation of alias {email} was successful!": "Erstellen des Alias {email} war erfolgreich!", "Creation of alias {email} was successful!": "Erstellen des Alias {email} war erfolgreich!",
"Alias {email} was deleted successfully": "Alias {email} wurde erfolgreich gelöscht", "Alias {email} was deleted successfully": "Alias {email} wurde erfolgreich gelöscht",

View File

@ -5,6 +5,7 @@ from .alias.create import AliasCreate
from .alias.delete import AliasDelete from .alias.delete import AliasDelete
from .alias.detail import AliasDetail from .alias.detail import AliasDetail
from .alias.edit_note import AliasEditNote from .alias.edit_note import AliasEditNote
from .alias.toggle_can_receive import AliasToggleCanReceive
from .alias.toggle_enabled import AliasToggleEnabled from .alias.toggle_enabled import AliasToggleEnabled
from .favicon import Favicon from .favicon import Favicon
from .robots import RobotsTXT from .robots import RobotsTXT
@ -15,6 +16,7 @@ from .user.detail import UserDetail
from .user.edit_note import UserEditNote from .user.edit_note import UserEditNote
from .user.list import UserList from .user.list import UserList
from .user.toggle_admin import UserToggleAdmin from .user.toggle_admin import UserToggleAdmin
from .user.toggle_can_receive import UserToggleCanReceive
from .user.toggle_enabled import UserToggleEnabled from .user.toggle_enabled import UserToggleEnabled
@ -36,6 +38,11 @@ def init_routes(app):
"user-toggle-enabled", "user-toggle-enabled",
UserToggleEnabled.as_view(), UserToggleEnabled.as_view(),
) )
app.add_url_rule(
"/user/<int:user_id>/toggle-can-receive",
"user-toggle-can-receive",
UserToggleCanReceive.as_view(),
)
app.add_url_rule( app.add_url_rule(
"/user/<int:user_id>/toggle-admin", "/user/<int:user_id>/toggle-admin",
"user-toggle-admin", "user-toggle-admin",
@ -57,6 +64,11 @@ def init_routes(app):
"alias-toggle-enabled", "alias-toggle-enabled",
AliasToggleEnabled.as_view(), AliasToggleEnabled.as_view(),
) )
app.add_url_rule(
"/alias/<int:alias_id>/toggle-can-receive",
"alias-toggle-can-receive",
AliasToggleCanReceive.as_view(),
)
app.add_url_rule( app.add_url_rule(
"/robots.txt", "/robots.txt",

View File

@ -38,6 +38,15 @@ class AliasCreate(
flash(t("Enabled is not set"), category="error") flash(t("Enabled is not set"), category="error")
error = True error = True
can_receive = self.request.form.get("can_receive")
if can_receive == "on":
self.can_receive = True
elif can_receive is None:
self.can_receive = False
else:
flash(t("Can receive is not set"), category="error")
error = True
note = self.request.form.get("note", "") note = self.request.form.get("note", "")
if not isinstance(note, str): if not isinstance(note, str):
flash(t("Note must be a string"), category="error") flash(t("Note must be a string"), category="error")
@ -49,7 +58,9 @@ class AliasCreate(
return # force re-rendering return # force re-rendering
try: try:
create_alias(self.email, self.user, self.enabled, self.note) create_alias(
self.email, self.user, self.enabled, self.can_receive, self.note
)
except EmailAlreadyExists as e: except EmailAlreadyExists as e:
if self.auth_user.is_admin: if self.auth_user.is_admin:
flash( flash(
@ -68,7 +79,7 @@ class AliasCreate(
def get_context(self): def get_context(self):
context = super().get_context() context = super().get_context()
for key in ("email", "enabled", "note"): for key in ("email", "enabled", "can_receive", "note"):
if hasattr(self, key): if hasattr(self, key):
context[key] = getattr(self, key) context[key] = getattr(self, key)
return context return context

View File

@ -0,0 +1,14 @@
from max.db import update_alias
from max.permissions import AllowAdminOrSelf
from max.views import BaseView
from ..base_alias_views import BackToAliasesMixin, FetchAliasMixin
class AliasToggleCanReceive(
AllowAdminOrSelf, FetchAliasMixin, BackToAliasesMixin, BaseView
):
def post(self):
update_alias(self.alias.id, can_receive=(not self.alias.can_receive))
_, back_url = self.get_back_text_and_url()
return self.redirect(back_url)

View File

@ -48,6 +48,15 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
flash(t("Enabled is not set"), category="error") flash(t("Enabled is not set"), category="error")
error = True error = True
can_receive = self.request.form.get("can_receive")
if can_receive == "on":
self.can_receive = True
elif can_receive is None:
self.can_receive = False
else:
flash(t("Can receive is not set"), category="error")
error = True
is_admin = self.request.form.get("is_admin") is_admin = self.request.form.get("is_admin")
if is_admin == "on": if is_admin == "on":
self.is_admin = True self.is_admin = True

View File

@ -0,0 +1,12 @@
from max.db import update_user
from max.permissions import AllowAdmin
from max.views import BaseView
from ..base_user_views import BackToUsersMixin, FetchUserMixin
class UserToggleCanReceive(AllowAdmin, FetchUserMixin, BackToUsersMixin, BaseView):
def post(self):
update_user(self.user.id, can_receive=(not self.user.can_receive))
_, back_url = self.get_back_text_and_url()
return self.redirect(back_url)

View File

@ -144,14 +144,21 @@ form {
margin-bottom: 8px; margin-bottom: 8px;
} }
.detail b { .detail b {
width: 4.5em;
display: inline-block; display: inline-block;
} }
.detail .note a {
margin-right: 1em;
margin-left: 1em;
vertical-align: top;
}
.detail .note b { .detail .note b {
margin-right: 1em;
margin-top: 0.5em;
vertical-align: top; vertical-align: top;
} }
.detail .note div { .detail .note div {
display: inline-block; display: inline-block;
margin-top: 0.5em;
} }
.detail form { .detail form {
margin: 0; margin: 0;
@ -174,6 +181,9 @@ form {
margin-top: 0; margin-top: 0;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.connection-properties .indent {
margin-left: 1.5em;
}
.checkbox { .checkbox {
padding: 8px; padding: 8px;

View File

@ -17,6 +17,13 @@
</label> </label>
</div> </div>
<div class="pure-controls checkbox">
<label for="can_receive">
<input id="can_receive" type="checkbox" name="can_receive" {% if can_receive %}checked{% endif %}>
{{ t("Can receive emails") }}
</label>
</div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="note">{{ t("Note") }}</label> <label for="note">{{ t("Note") }}</label>
<textarea id="note" name="note" form="form">{% if note is defined %}{{ note }}{% endif %}</textarea> <textarea id="note" name="note" form="form">{% if note is defined %}{{ note }}{% endif %}</textarea>

View File

@ -8,13 +8,19 @@
<div> <div>
<b>{{ t("Enabled") }}:</b> <b>{{ t("Enabled") }}:</b>
{{ macros.checkbox(alias, "enabled", url_for('alias-toggle-enabled', alias_id=alias.id, return='detail')) }} {{ macros.checkbox(alias, "enabled", url_for('alias-toggle-enabled', alias_id=alias.id, return='detail')) }}
</div><div class="note"> </div>
<div>
<b>{{ t("Can receive emails") }}:</b>
{{ macros.checkbox(alias, "can_receive", url_for('alias-toggle-can-receive', alias_id=alias.id, return='detail')) }}
</div>
<div class="note">
<b>{{ t("Note") }}:</b><div>{{ macros.format_note(alias) }}</div> <b>{{ t("Note") }}:</b><div>{{ macros.format_note(alias) }}</div>
<a href="{{ url_for('alias-edit-note', alias_id=alias.id, return='detail') }}" class="button-small pure-button optional"> <a href="{{ url_for('alias-edit-note', alias_id=alias.id, return='detail') }}" class="button-small pure-button optional">
<i class="fa fa-pen"></i> <i class="fa fa-pen"></i>
{{ t("Edit") }} {{ t("Edit") }}
</a> </a>
</div><div> </div>
<div>
<a href="{{ url_for('alias-delete', alias_id=alias.id, return='detail') }}" class="button-small button-error pure-button"> <a href="{{ url_for('alias-delete', alias_id=alias.id, return='detail') }}" class="button-small button-error pure-button">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
{{ t("Delete") }} {{ t("Delete") }}

View File

@ -27,6 +27,13 @@
</label> </label>
</div> </div>
<div class="pure-controls checkbox">
<label for="can_receive">
<input id="can_receive" type="checkbox" name="can_receive" {% if can_receive %}checked{% endif %}>
{{ t("Can receive emails") }}
</label>
</div>
<div class="pure-controls checkbox"> <div class="pure-controls checkbox">
<label for="is_admin"> <label for="is_admin">
<input id="is_admin" type="checkbox" name="is_admin" {% if is_admin %}checked{% endif %}> <input id="is_admin" type="checkbox" name="is_admin" {% if is_admin %}checked{% endif %}>

View File

@ -20,6 +20,10 @@
<b>{{ t("Enabled") }}:</b> <b>{{ t("Enabled") }}:</b>
{{ macros.checkbox(user, "enabled", url_for('user-toggle-enabled', user_id=user.id, return='detail')) }} {{ macros.checkbox(user, "enabled", url_for('user-toggle-enabled', user_id=user.id, return='detail')) }}
</div> </div>
<div>
<b>{{ t("Can receive emails") }}:</b>
{{ macros.checkbox(user, "can_receive", url_for('user-toggle-can-receive', user_id=user.id, return='detail')) }}
</div>
<div> <div>
<b>{{ t("Admin") }}:</b> <b>{{ t("Admin") }}:</b>
{{ macros.checkbox(user, "is_admin", url_for('user-toggle-admin', user_id=user.id, return='detail')) }} {{ macros.checkbox(user, "is_admin", url_for('user-toggle-admin', user_id=user.id, return='detail')) }}
@ -53,10 +57,14 @@
<details class="connection-properties"> <details class="connection-properties">
<summary>{{ t("Email connection properties") }}</summary> <summary>{{ t("Email connection properties") }}</summary>
<p>IMAP: mail.finn.st, Port: 993</p> <p>IMAP: finn.st, Port 993</p>
<p>username: {{ user.email }}</p> <p class="indent">Username: {{ user.email }}</p>
<p>Connection security: SSL/TLS</p> <p class="indent">Connection security: SSL/TLS</p>
<p>Authentication method: password</p> <p class="indent">Authentication method: password</p>
<p>SMTP: finn.st, Port 587</p>
<p class="indent">Username: {{ user.email }}</p>
<p class="indent">Connection security: STARTTLS</p>
<p class="indent">Authentication method: password</p>
</details> </details>
{% if aliases|length > 0 %} {% if aliases|length > 0 %}
@ -66,6 +74,7 @@
<tr> <tr>
<th>{{ t("Email") }}</th> <th>{{ t("Email") }}</th>
<th>{{ t("Enabled") }}</th> <th>{{ t("Enabled") }}</th>
<th>{{ t("Can receive emails") }}</th>
<th>{{ t("Note") }}</th> <th>{{ t("Note") }}</th>
<th class="optional">{{ t("Actions") }}</th> <th class="optional">{{ t("Actions") }}</th>
</tr> </tr>
@ -80,6 +89,9 @@
<td> <td>
{{ macros.checkbox(alias, "enabled", url_for('alias-toggle-enabled', alias_id=alias.id)) }} {{ macros.checkbox(alias, "enabled", url_for('alias-toggle-enabled', alias_id=alias.id)) }}
</td> </td>
<td>
{{ macros.checkbox(alias, "can_receive", url_for('alias-toggle-can-receive', alias_id=alias.id)) }}
</td>
<td> <td>
{{ macros.format_note(alias) }} {{ macros.format_note(alias) }}
<a href="{{ url_for('alias-edit-note', alias_id=alias.id) }}" class="button-small pure-button optional"> <a href="{{ url_for('alias-edit-note', alias_id=alias.id) }}" class="button-small pure-button optional">

View File

@ -20,6 +20,7 @@
<tr> <tr>
<th>{{ t("Email") }}</th> <th>{{ t("Email") }}</th>
<th>{{ t("Enabled") }}</th> <th>{{ t("Enabled") }}</th>
<th>{{ t("Can receive emails") }}</th>
<th>{{ t("Admin") }}</th> <th>{{ t("Admin") }}</th>
<th>{{ t("Note") }}</th> <th>{{ t("Note") }}</th>
<th class="optional">{{ t("Actions") }}</th> <th class="optional">{{ t("Actions") }}</th>
@ -35,6 +36,9 @@
<td> <td>
{{ macros.checkbox(user, "enabled", url_for('user-toggle-enabled', user_id=user.id)) }} {{ macros.checkbox(user, "enabled", url_for('user-toggle-enabled', user_id=user.id)) }}
</td> </td>
<td>
{{ macros.checkbox(user, "can_receive", url_for('user-toggle-can-receive', user_id=user.id)) }}
</td>
<td> <td>
{{ macros.checkbox(user, "is_admin", url_for('user-toggle-admin', user_id=user.id)) }} {{ macros.checkbox(user, "is_admin", url_for('user-toggle-admin', user_id=user.id)) }}
</td> </td>

2
scripts/psql.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
PGPASSWORD="$PGSQL_PASSWORD" psql -h "$PGSQL_HOST" -p "$PGSQL_PORT" -U "$PGSQL_USER" -d "$PGSQL_NAME"