Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79529a4aac |
77
.env.dist
Normal file
77
.env.dist
Normal file
@@ -0,0 +1,77 @@
|
||||
# Mailu main configuration file
|
||||
#
|
||||
# Most configuration variables can be modified through the Web interface,
|
||||
# these few settings must however be configured before starting the mail
|
||||
# server and require a restart upon change.
|
||||
|
||||
###################################
|
||||
# Common configuration variables
|
||||
###################################
|
||||
|
||||
# Set this to the path where Mailu data and configuration is stored
|
||||
ROOT=/mailu
|
||||
|
||||
# Mailu version to run (stable, 1.0, 1.1, etc. or latest)
|
||||
VERSION=stable
|
||||
|
||||
# Set to a randomly generated 16 bytes string
|
||||
SECRET_KEY=ChangeMeChangeMe
|
||||
|
||||
# Address where listening ports should bind
|
||||
BIND_ADDRESS=127.0.0.1
|
||||
|
||||
# Main mail domain
|
||||
DOMAIN=mailu.io
|
||||
|
||||
# Exposed mail-server hostname
|
||||
HOSTNAME=mail.mailu.io
|
||||
|
||||
# Postmaster local part (will append the main mail domain)
|
||||
POSTMASTER=admin
|
||||
|
||||
# Docker-compose project name, this will prepended to containers names.
|
||||
COMPOSE_PROJECT_NAME=mailu
|
||||
|
||||
###################################
|
||||
# Optional features
|
||||
###################################
|
||||
|
||||
# Choose which frontend Web server to run if any (value: nginx, none)
|
||||
FRONTEND=none
|
||||
|
||||
# Choose which webmail to run if any (values: roundcube, rainloop, none)
|
||||
WEBMAIL=none
|
||||
|
||||
# Expose the admin interface in publicly (values: yes, no)
|
||||
EXPOSE_ADMIN=no
|
||||
|
||||
# Use Letsencrypt to generate a TLS certificate (uncomment to enable)
|
||||
# ENABLE_CERTBOT=True
|
||||
|
||||
# Dav server implementation (value: radicale, none)
|
||||
WEBDAV=none
|
||||
|
||||
###################################
|
||||
# Mail settings
|
||||
###################################
|
||||
|
||||
# Message size limit in bytes
|
||||
# Default: accept messages up to 50MB
|
||||
MESSAGE_SIZE_LIMIT=50000000
|
||||
|
||||
# Networks granted relay permissions, make sure that you include your Docker
|
||||
# internal network (default to 172.17.0.0/16)
|
||||
RELAYNETS=172.16.0.0/12
|
||||
|
||||
# Will relay all outgoing mails if configured
|
||||
RELAYHOST=
|
||||
|
||||
# Fetchmail delay
|
||||
FETCHMAIL_DELAY=600
|
||||
|
||||
###################################
|
||||
# Developers
|
||||
###################################
|
||||
|
||||
# Uncomment this to enable debugging globally
|
||||
# DEBUG=True
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,15 +1,12 @@
|
||||
*.pyc
|
||||
*.mo
|
||||
__pycache__
|
||||
/admin/lib
|
||||
/admin/bin
|
||||
/admin/include
|
||||
pip-selfcheck.json
|
||||
/core/admin/lib*
|
||||
/core/admin/bin
|
||||
/core/admin/include
|
||||
/docs/lib*
|
||||
/docs/bin
|
||||
/docs/include
|
||||
/docs/_build
|
||||
/.env
|
||||
/data
|
||||
/docker-compose.mac.yml
|
||||
/docker-compose.yml
|
||||
/.idea
|
||||
/.vscode
|
||||
/.idea
|
||||
@@ -1,30 +0,0 @@
|
||||
splits:
|
||||
- prefix: "admin"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Admin.git"
|
||||
- prefix: "clamav"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/ClamAV.git"
|
||||
- prefix: "dovecot"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Dovecot.git"
|
||||
- prefix: "fetchmail"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Fetchmail.git"
|
||||
- prefix: "nginx-no-https"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/NGINX-no-HTTPS.git"
|
||||
- prefix: "nginx"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/NGINX.git"
|
||||
- prefix: "postfix"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Postfix.git"
|
||||
- prefix: "radicale"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Radicale.git"
|
||||
- prefix: "rainloop"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/RainLoop.git"
|
||||
- prefix: "rmilter"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Rmilter.git"
|
||||
- prefix: "roundcube"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Roundcube.git"
|
||||
- prefix: "rspamd"
|
||||
target: "https://${GH_TOKEN}@github.com/Mailu/Rspamd.git"
|
||||
|
||||
origins:
|
||||
- ^master$
|
||||
- ^stable$
|
||||
- ^v\d+\.\d+\.\d+$
|
||||
29
.travis.yml
29
.travis.yml
@@ -1,29 +0,0 @@
|
||||
sudo: required
|
||||
services: docker
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- docker-ce
|
||||
|
||||
env:
|
||||
- MAILU_VERSION=$TRAVIS_BRANCH
|
||||
language: python
|
||||
python:
|
||||
- "3.6"
|
||||
install:
|
||||
- sudo curl -L https://github.com/docker/compose/releases/download/1.23.0-rc3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
- sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
before_script:
|
||||
- docker-compose -v
|
||||
- docker-compose -f tests/build.yml build
|
||||
|
||||
script:
|
||||
- /bin/true
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: bash tests/deploy.sh
|
||||
on:
|
||||
all_branches: true
|
||||
condition: -n $DOCKER_UN
|
||||
14
AUTHORS.md
14
AUTHORS.md
@@ -6,19 +6,7 @@ If you contribute time, code or resources to the project, feel free to add
|
||||
your name, pseudonym, and any contact information you feel is relevant to
|
||||
this file.
|
||||
|
||||
As it is almost impossible to distinguish code contributions from various
|
||||
authors, all are considered equal contributors and all must agree with
|
||||
any change in the software license.
|
||||
|
||||
Other contributors:
|
||||
|
||||
- "Angedestenebres" - Tests on development version & Current version
|
||||
- Angedestenebres - Tests on development version & Current version
|
||||
- Stefan Auditor - German translation on POEditor.com
|
||||
- [Carlos Bernárdez](https://github.com/jkarlosb) - [[Contributions in Mailu]](https://github.com/Mailu/Mailu/commits?author=jkarlosb)
|
||||
- Felipe Lubra - Portugese translation
|
||||
- Mélanie Henry - German translation
|
||||
- Maxime Saddok - French translation
|
||||
- "ofthesun9" - French translation
|
||||
- "SunMar" - Dutch translation
|
||||
- "Marty Hou" - Chinese Simple translation
|
||||
- [Thomas Sänger](https://github.com/HorayNarea) - German translation
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -5,65 +5,6 @@ Notable changes to this project are documented in the current file. For more
|
||||
details about individual changes, see the Git log. You should read this before
|
||||
upgrading Freposte.io as some changes will include useful notes.
|
||||
|
||||
v1.5.1 - 2017-11-21
|
||||
-------------------
|
||||
|
||||
- Global: add a DNS-based instance count tracker, use the ``DISABLE_STATISTICS``
|
||||
setting to disable it.
|
||||
- Global: specify container dependencies in the Compose configuration, update
|
||||
your ``docker-compose.yml``.
|
||||
- Feature: add a *mail* TLS flavor that only enforces TLS for email connections.
|
||||
- Feature: welcome emails, see the configuration for details
|
||||
- Feature: end date for vacations, see the automatic reply page
|
||||
- L10N: dutch loca is now available
|
||||
- L10N: swedish loca is now available
|
||||
- L10N: italian loca is now partially available
|
||||
- L10N: chinese loca is now available
|
||||
- Upstream: upgrade to Roundcube 1.3.3
|
||||
- Enhancement: use the alpine image for redis
|
||||
- Enhancement: use a dynamic worker count for Nginx
|
||||
- Bug: fix the pop3 proxy
|
||||
- Bug: fix DNS resolution bugs in the frontend
|
||||
- Bug: fix Webdav authentication
|
||||
- Bug: properly honor enabled features (imap and pop3) per user
|
||||
|
||||
v1.5.0 - 2017-11-05
|
||||
-------------------
|
||||
|
||||
- Global: clean the ``.env`` file and change many options, *make sure
|
||||
that you download the latest ``.env`` and apply your settings when migrating.*
|
||||
- Global: clean the Compose configuration, *make sure that you download the
|
||||
latest ``docker-compose.yml`` when migrating.*
|
||||
- Global: nginx is now a reverse proxy for HTTP, SMTP, IMAP and POP.
|
||||
- Global: the new Rainloop webmail is available.
|
||||
- Global: the mail stack now supports IPv6.
|
||||
- Global: most images moved to Alpine.
|
||||
- Global: the documentation moved to a Sphinx directory.
|
||||
- Global: deprecate rmilter and use rspamd proxy instead.
|
||||
- Feature: multiple TLS flavors are available, see the ``TLS_FLAVOR`` setting.
|
||||
- Feature: alternative domains now act as a copy of a given domain.
|
||||
- Feature: relay domains now act as a mail relay (e.g. for backup servers).
|
||||
- Feature: the server now supports multiple public names, with letsencrypt.
|
||||
- Feature: authentication tokens can be generated per client.
|
||||
- Feature: the manage.py CLI has many options to import and manage a setup.
|
||||
- Feature: add overrides for the Postfix configuration.
|
||||
- Feature: allow to keep or discard forwarded messages.
|
||||
- Feature: make password encryption scheme configurable.
|
||||
- Feature: make DMARC rua configurable.
|
||||
- Feature: Clamav may now be disabled completely.
|
||||
- Feature: support a configurable recipient delimiter for address extension.
|
||||
- Feature: the admin interface points to the webmail and a configurable site.
|
||||
- L10N: portugese loca is now available
|
||||
- Upstream: upgrade to Roundcube 1.3.2
|
||||
- Upstream: upgrade to Rainloop 1.11.3
|
||||
- Upstream: upgrade to Dovecot 2.2.33
|
||||
- Upstream: upgrade to Postfix 3.2.4
|
||||
- Bug: the Postfix queue is now persisted.
|
||||
- Bug: certbot now handle renewal properly.
|
||||
- Bug: fix sender and recipient restrictions for antispam features.
|
||||
- Bug: webmails now handle large attachments.
|
||||
- Bug: dhparam are now generated properly on the frontend.
|
||||
|
||||
v1.4.0 - 2017-02-12
|
||||
-------------------
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* Accepting that not all community members share our view of the project
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at team@mailu.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
@@ -1,7 +0,0 @@
|
||||
This project is open source, and your contributions are all welcome. There are mostly three different ways one can contribute to the project:
|
||||
|
||||
1. use Mailu, either on test or on production instances, and report meaningful bugs when you find some;
|
||||
2. contribute code and/or configuration to the repository (see [the development guidelines](https://mailu.io/contributors/guide.html) for details);
|
||||
3. contribute localization to your native language (see [the localization docs](https://mailu.io/contributors/localization.html) for details);
|
||||
|
||||
Either way, keep in mind that the code you write or the translation you produce muts be licensed under the same conditions as the project itself. Additionally, all contributors are considered equal co-authors of the project.
|
||||
56
README.md
56
README.md
@@ -1,15 +1,28 @@
|
||||

|
||||

|
||||
|
||||
Mailu is a simple yet full-featured mail server as a set of Docker images.
|
||||
It is free software (both as in free beer and as in free speech), open to
|
||||
suggestions and external contributions. The project aims at providing people
|
||||
with an easily setup, easily maintained and full-featured mail server while
|
||||
not shipping proprietary software nor unrelated features often found in
|
||||
popular groupware.
|
||||
[Join us and chat about the project.](https://riot.im/app/#/room/#mailu:tedomum.net)
|
||||
|
||||
Most of the documentation is available on our [Website](https://mailu.io),
|
||||
you can also [try our demo server](https://mailu.io/master/demo.html)
|
||||
before setting up your own, and come [talk to us on Matrix](https://matrix.to/#/#mailu:tedomum.net).
|
||||
Mailu
|
||||
=====
|
||||
|
||||
*This project used to be named Freeposte.io, the name was changed back in
|
||||
October 2016.*
|
||||
|
||||
Simple yet full-featured mail server as a set of Docker images.
|
||||
The idea behing Mailu is identical to motivations that led to poste.io:
|
||||
providing a simple and maintainable mail server that is painless to manage and
|
||||
does not require more resources than necessary.
|
||||
|
||||
People from poste.io did an amazing job at accomplishing this ; any company
|
||||
looking for a serious yet simple mail server with professional support should
|
||||
turn to them.
|
||||
|
||||
This project is meant for free software supporters and hackers to reach the
|
||||
same level of functionality and still be able to host a complete mail server
|
||||
at little cost while running only FOSS, applying the KISS principle and being
|
||||
able to fine-tune some details if needed.
|
||||
|
||||
[Try it out on our demo server](https://github.com/mail-u/mailu/wiki/Demo-server).
|
||||
|
||||
Features
|
||||
========
|
||||
@@ -17,15 +30,28 @@ Features
|
||||
Main features include:
|
||||
|
||||
- **Standard email server**, IMAP and IMAP+, SMTP and Submission
|
||||
- **Advanced email features**, aliases, domain aliases, custom routing
|
||||
- **Web access**, multiple Webmails and adminitration interface
|
||||
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
|
||||
- **Admin features**, global admins, announcements, per-domain delegation, quotas
|
||||
- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner
|
||||
- **Admin features**, global admins, per-domain delegation, quotas
|
||||
- **Security**, enforced TLS, outgoing DKIM, anti-virus scanner
|
||||
- **Antispam**, auto-learn, greylisting, DMARC and SPF
|
||||
- **Freedom**, all FOSS components, no tracker included
|
||||
|
||||

|
||||

|
||||
|
||||
Running a mail server
|
||||
=====================
|
||||
|
||||
Mailu runs on top of Docker for easy packaging and upgrades. All you need
|
||||
is a proper system with Docker and Compose installed, then simply download
|
||||
the ``docker-compose.yml`` and sample ``.env``, tune them to your needs and
|
||||
fire up the mail server:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
For a detailed walktrough, see the [Setup Guide](https://github.com/mail-u/mailu/wiki/Setup-Guide).
|
||||
|
||||
Contributing
|
||||
============
|
||||
@@ -34,3 +60,5 @@ Mailu is free software, open to suggestions and contributions. All
|
||||
components are free software and compatible with the MIT license. All
|
||||
specific configuration files, Dockerfiles and code are placed under the
|
||||
MIT license.
|
||||
|
||||
For details, see the [Contributor Guide](https://github.com/mail-u/mailu/wiki/Contributors-Guide).
|
||||
|
||||
16
admin/Dockerfile
Normal file
16
admin/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY mailu ./mailu
|
||||
COPY migrations ./migrations
|
||||
COPY manage.py .
|
||||
COPY start.sh /start.sh
|
||||
|
||||
RUN pybabel compile -d mailu/translations
|
||||
|
||||
CMD ["/start.sh"]
|
||||
73
admin/mailu/__init__.py
Normal file
73
admin/mailu/__init__.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import flask
|
||||
import flask_sqlalchemy
|
||||
import flask_bootstrap
|
||||
import flask_login
|
||||
import flask_script
|
||||
import flask_migrate
|
||||
import flask_babel
|
||||
|
||||
import os
|
||||
import docker
|
||||
|
||||
from apscheduler.schedulers import background
|
||||
|
||||
|
||||
# Create application
|
||||
app = flask.Flask(__name__, static_url_path='/admin/app_static')
|
||||
|
||||
default_config = {
|
||||
'SQLALCHEMY_DATABASE_URI': 'sqlite:////data/main.db',
|
||||
'SQLALCHEMY_TRACK_MODIFICATIONS': False,
|
||||
'SECRET_KEY': 'changeMe',
|
||||
'DOCKER_SOCKET': 'unix:///var/run/docker.sock',
|
||||
'HOSTNAME': 'mail.mailu.io',
|
||||
'DOMAIN': 'mailu.io',
|
||||
'POSTMASTER': 'postmaster',
|
||||
'DEBUG': False,
|
||||
'BOOTSTRAP_SERVE_LOCAL': True,
|
||||
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
|
||||
'DKIM_SELECTOR': 'dkim',
|
||||
'BABEL_DEFAULT_LOCALE': 'en',
|
||||
'BABEL_DEFAULT_TIMEZONE': 'UTC',
|
||||
'ENABLE_CERTBOT': False,
|
||||
'CERTS_PATH': '/certs'
|
||||
}
|
||||
|
||||
# Load configuration from the environment if available
|
||||
for key, value in default_config.items():
|
||||
app.config[key] = os.environ.get(key, value)
|
||||
|
||||
# Setup components
|
||||
flask_bootstrap.Bootstrap(app)
|
||||
db = flask_sqlalchemy.SQLAlchemy(app)
|
||||
migrate = flask_migrate.Migrate(app, db)
|
||||
login_manager = flask_login.LoginManager()
|
||||
login_manager.init_app(app)
|
||||
babel = flask_babel.Babel(app)
|
||||
translations = list(map(str, babel.list_translations()))
|
||||
scheduler = background.BackgroundScheduler()
|
||||
|
||||
# Manager commnad
|
||||
manager = flask_script.Manager(app)
|
||||
manager.add_command('db', flask_migrate.MigrateCommand)
|
||||
|
||||
# Task scheduling
|
||||
if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
scheduler.start()
|
||||
|
||||
# Babel configuration
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
return flask.request.accept_languages.best_match(translations)
|
||||
|
||||
# Certbot configuration
|
||||
if app.config['ENABLE_CERTBOT']:
|
||||
from mailu import certbot
|
||||
|
||||
# Finally setup the blueprint and redirect /
|
||||
from mailu import admin
|
||||
app.register_blueprint(admin.app, url_prefix='/admin')
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return flask.redirect(flask.url_for("admin.index"))
|
||||
31
admin/mailu/admin/__init__.py
Normal file
31
admin/mailu/admin/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from flask import Blueprint
|
||||
from mailu import login_manager, db
|
||||
|
||||
import flask_login
|
||||
|
||||
|
||||
app = Blueprint(
|
||||
'admin', __name__,
|
||||
template_folder='templates',
|
||||
static_folder='static')
|
||||
|
||||
# Import models
|
||||
from mailu.admin import models
|
||||
|
||||
# Register the login components
|
||||
login_manager.login_view = "admin.login"
|
||||
login_manager.user_loader(models.User.query.get)
|
||||
|
||||
@app.context_processor
|
||||
def inject_user():
|
||||
return dict(current_user=flask_login.current_user)
|
||||
|
||||
# Import views
|
||||
from mailu.admin.views import \
|
||||
admins, \
|
||||
managers, \
|
||||
base, \
|
||||
aliases, \
|
||||
users, \
|
||||
domains, \
|
||||
fetches
|
||||
@@ -1,5 +1,4 @@
|
||||
from mailu import db, models
|
||||
from mailu.ui import forms
|
||||
from mailu.admin import db, models, forms
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
@@ -51,18 +51,6 @@ class DomainForm(flask_wtf.FlaskForm):
|
||||
submit = fields.SubmitField(_('Create'))
|
||||
|
||||
|
||||
class AlternativeForm(flask_wtf.FlaskForm):
|
||||
name = fields.StringField(_('Alternative name'), [validators.DataRequired()])
|
||||
submit = fields.SubmitField(_('Create'))
|
||||
|
||||
|
||||
class RelayForm(flask_wtf.FlaskForm):
|
||||
name = fields.StringField(_('Relayed domain name'), [validators.DataRequired()])
|
||||
smtp = fields.StringField(_('Remote host'))
|
||||
comment = fields.StringField(_('Comment'))
|
||||
submit = fields.SubmitField(_('Create'))
|
||||
|
||||
|
||||
class UserForm(flask_wtf.FlaskForm):
|
||||
localpart = fields.StringField(_('E-mail'), [validators.DataRequired()])
|
||||
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
|
||||
@@ -77,7 +65,7 @@ class UserForm(flask_wtf.FlaskForm):
|
||||
class UserSettingsForm(flask_wtf.FlaskForm):
|
||||
displayed_name = fields.StringField(_('Displayed name'))
|
||||
spam_enabled = fields.BooleanField(_('Enable spam filter'))
|
||||
spam_threshold = fields_.IntegerSliderField(_('Spam filter tolerance'))
|
||||
spam_threshold = fields_.IntegerSliderField(_('Spam filter threshold'))
|
||||
submit = fields.SubmitField(_('Save settings'))
|
||||
|
||||
|
||||
@@ -89,7 +77,6 @@ class UserPasswordForm(flask_wtf.FlaskForm):
|
||||
|
||||
class UserForwardForm(flask_wtf.FlaskForm):
|
||||
forward_enabled = fields.BooleanField(_('Enable forwarding'))
|
||||
forward_keep = fields.BooleanField(_('Keep a copy of the emails'))
|
||||
forward_destination = fields.StringField(
|
||||
_('Destination'), [validators.Optional(), validators.Email()]
|
||||
)
|
||||
@@ -101,22 +88,9 @@ class UserReplyForm(flask_wtf.FlaskForm):
|
||||
reply_subject = fields.StringField(_('Reply subject'))
|
||||
reply_body = fields.StringField(_('Reply body'),
|
||||
widget=widgets.TextArea())
|
||||
reply_enddate = fields.html5.DateField(_('End of vacation'))
|
||||
submit = fields.SubmitField(_('Update'))
|
||||
|
||||
|
||||
class TokenForm(flask_wtf.FlaskForm):
|
||||
displayed_password = fields.StringField(
|
||||
_('Your token (write it down, as it will never be displayed again)')
|
||||
)
|
||||
raw_password = fields.HiddenField([validators.DataRequired()])
|
||||
comment = fields.StringField(_('Comment'))
|
||||
ip = fields.StringField(
|
||||
_('Authorized IP'), [validators.Optional(), validators.IPAddress()]
|
||||
)
|
||||
submit = fields.SubmitField(_('Create'))
|
||||
|
||||
|
||||
class AliasForm(flask_wtf.FlaskForm):
|
||||
localpart = fields.StringField(_('Alias'), [validators.DataRequired()])
|
||||
wildcard = fields.BooleanField(
|
||||
@@ -1,16 +1,14 @@
|
||||
from mailu import app, db, dkim, login_manager
|
||||
from mailu.admin import db, dkim
|
||||
from mailu import app
|
||||
|
||||
from sqlalchemy.ext import declarative
|
||||
from passlib import context, hash
|
||||
from datetime import datetime, date
|
||||
from email.mime import text
|
||||
|
||||
from passlib import context
|
||||
from datetime import datetime
|
||||
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
import glob
|
||||
import smtplib
|
||||
|
||||
|
||||
# Many-to-many association table for domain managers
|
||||
@@ -102,36 +100,6 @@ class Domain(Base):
|
||||
return False
|
||||
|
||||
|
||||
class Alternative(Base):
|
||||
""" Alternative name for a served domain.
|
||||
The name "domain alias" was avoided to prevent some confusion.
|
||||
"""
|
||||
|
||||
__tablename__ = "alternative"
|
||||
|
||||
name = db.Column(db.String(80), primary_key=True, nullable=False)
|
||||
domain_name = db.Column(db.String(80), db.ForeignKey(Domain.name))
|
||||
domain = db.relationship(Domain,
|
||||
backref=db.backref('alternatives', cascade='all, delete-orphan'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Relay(Base):
|
||||
""" Relayed mail domain.
|
||||
The domain is either relayed publicly or through a specified SMTP host.
|
||||
"""
|
||||
|
||||
__tablename__ = "relay"
|
||||
|
||||
name = db.Column(db.String(80), primary_key=True, nullable=False)
|
||||
smtp = db.Column(db.String(80), nullable=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Email(object):
|
||||
""" Abstraction for an email address (localpart and domain).
|
||||
"""
|
||||
@@ -152,22 +120,10 @@ class Email(object):
|
||||
context.current_parameters["localpart"],
|
||||
context.current_parameters["domain_name"],
|
||||
)
|
||||
return db.Column(db.String(255, collation="NOCASE"),
|
||||
return db.Column(db.String(255),
|
||||
primary_key=True, nullable=False,
|
||||
default=updater)
|
||||
|
||||
def sendmail(self, subject, body):
|
||||
""" Send an email to the address.
|
||||
"""
|
||||
from_address = '{}@{}'.format(
|
||||
app.config['POSTMASTER'], app.config['DOMAIN'])
|
||||
with smtplib.SMTP('smtp', port=10025) as smtp:
|
||||
msg = text.MIMEText(body)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = from_address
|
||||
msg['To'] = self.email
|
||||
smtp.sendmail(from_address, [self.email], msg.as_string())
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
@@ -190,12 +146,9 @@ class User(Base, Email):
|
||||
# Filters
|
||||
forward_enabled = db.Column(db.Boolean(), nullable=False, default=False)
|
||||
forward_destination = db.Column(db.String(255), nullable=True, default=None)
|
||||
forward_keep = db.Column(db.Boolean(), nullable=False, default=True)
|
||||
reply_enabled = db.Column(db.Boolean(), nullable=False, default=False)
|
||||
reply_subject = db.Column(db.String(255), nullable=True, default=None)
|
||||
reply_body = db.Column(db.Text(), nullable=True, default=None)
|
||||
reply_enddate = db.Column(db.Date, nullable=False,
|
||||
default=date(2999, 12, 31))
|
||||
|
||||
# Settings
|
||||
displayed_name = db.Column(db.String(160), nullable=False, default="")
|
||||
@@ -210,28 +163,16 @@ class User(Base, Email):
|
||||
def get_id(self):
|
||||
return self.email
|
||||
|
||||
scheme_dict = {'SHA512-CRYPT': "sha512_crypt",
|
||||
'SHA256-CRYPT': "sha256_crypt",
|
||||
'MD5-CRYPT': "md5_crypt",
|
||||
'CRYPT': "des_crypt"}
|
||||
pw_context = context.CryptContext(
|
||||
schemes = scheme_dict.values(),
|
||||
default=scheme_dict[app.config['PASSWORD_SCHEME']],
|
||||
["sha512_crypt", "sha256_crypt", "md5_crypt"]
|
||||
)
|
||||
|
||||
def check_password(self, password):
|
||||
reference = re.match('({[^}]+})?(.*)', self.password).group(2)
|
||||
return User.pw_context.verify(password, reference)
|
||||
|
||||
def set_password(self, password, hash_scheme=app.config['PASSWORD_SCHEME'], raw=False):
|
||||
"""Set password for user with specified encryption scheme
|
||||
@password: plain text password to encrypt (if raw == True the hash itself)
|
||||
"""
|
||||
# for the list of hash schemes see https://wiki2.dovecot.org/Authentication/PasswordSchemes
|
||||
if raw:
|
||||
self.password = '{'+hash_scheme+'}' + password
|
||||
else:
|
||||
self.password = '{'+hash_scheme+'}' + User.pw_context.encrypt(password, self.scheme_dict[hash_scheme])
|
||||
def set_password(self, password):
|
||||
self.password = '{SHA512-CRYPT}' + User.pw_context.encrypt(password)
|
||||
|
||||
def get_managed_domains(self):
|
||||
if self.global_admin:
|
||||
@@ -247,18 +188,11 @@ class User(Base, Email):
|
||||
emails.extend(domain.aliases)
|
||||
return emails
|
||||
|
||||
def send_welcome(self):
|
||||
if app.config["WELCOME"].lower() == "true":
|
||||
self.sendmail(app.config["WELCOME_SUBJECT"],
|
||||
app.config["WELCOME_BODY"])
|
||||
|
||||
@classmethod
|
||||
def login(cls, email, password):
|
||||
user = cls.query.get(email)
|
||||
return user if (user and user.check_password(password)) else None
|
||||
|
||||
login_manager.user_loader(User.query.get)
|
||||
|
||||
|
||||
class Alias(Base, Email):
|
||||
""" An alias is an email address that redirects to some destination.
|
||||
@@ -271,29 +205,6 @@ class Alias(Base, Email):
|
||||
destination = db.Column(CommaSeparatedList, nullable=False, default=[])
|
||||
|
||||
|
||||
class Token(Base):
|
||||
""" A token is an application password for a given user.
|
||||
"""
|
||||
__tablename__ = "token"
|
||||
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
user_email = db.Column(db.String(255), db.ForeignKey(User.email),
|
||||
nullable=False)
|
||||
user = db.relationship(User,
|
||||
backref=db.backref('tokens', cascade='all, delete-orphan'))
|
||||
password = db.Column(db.String(255), nullable=False)
|
||||
ip = db.Column(db.String(255))
|
||||
|
||||
def check_password(self, password):
|
||||
return hash.sha256_crypt.verify(password, self.password)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = hash.sha256_crypt.using(rounds=1000).hash(password)
|
||||
|
||||
def __str__(self):
|
||||
return self.comment
|
||||
|
||||
|
||||
class Fetch(Base):
|
||||
""" A fetched account is a repote POP/IMAP account fetched into a local
|
||||
account.
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
@@ -4,8 +4,7 @@
|
||||
{% trans %}Add a global administrator{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.box() %}
|
||||
{% block box_content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.admin, id='admin') }}
|
||||
@@ -14,5 +13,4 @@
|
||||
$("#admin").select2();
|
||||
</script>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
30
admin/mailu/admin/templates/admin/list.html
Normal file
30
admin/mailu/admin/templates/admin/list.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Global administrators{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
<a class="btn btn-primary" href="{{ url_for('.admin_create') }}">
|
||||
{% trans %}Add administrator{% endtrans %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block box %}
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
<th>{% trans %}Email{% endtrans %}</th>
|
||||
</tr>
|
||||
{% for admin in admins %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
<td>{{ admin }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -8,8 +8,7 @@
|
||||
{{ domain }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.box() %}
|
||||
{% block box_content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }}
|
||||
@@ -24,5 +23,4 @@
|
||||
})
|
||||
</script>
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
41
admin/mailu/admin/templates/alias/list.html
Normal file
41
admin/mailu/admin/templates/alias/list.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Alias list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{{ domain.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
<a class="btn btn-primary" href="{{ url_for('.alias_create', domain_name=domain.name) }}">{% trans %}Add alias{% endtrans %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block box %}
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
<th>{% trans %}Email{% endtrans %}</th>
|
||||
<th>{% trans %}Destination{% endtrans %}</th>
|
||||
<th>{% trans %}Comment{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
<th>{% trans %}Last edit{% endtrans %}</th>
|
||||
</tr>
|
||||
{% for alias in domain.aliases %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>
|
||||
<a href="{{ url_for('.alias_delete', alias=alias.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
<td>{{ alias }}</td>
|
||||
<td>{{ alias.destination|join(', ') or '-' }}</td>
|
||||
<td>{{ alias.comment or '' }}</td>
|
||||
<td>{{ alias.created_at }}</td>
|
||||
<td>{{ alias.updated_at or '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -4,13 +4,15 @@
|
||||
{% trans %}Public announcement{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.box() %}
|
||||
{% block subtitle %}
|
||||
{% trans %}from{% endtrans %} {{ from_address }}
|
||||
{% endblock %}
|
||||
|
||||
{% block box_content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.announcement_subject) }}
|
||||
{{ macros.form_field(form.announcement_body, rows=10) }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -29,7 +29,7 @@ class="hold-transition skin-blue sidebar-mini"
|
||||
{% block navbar %}
|
||||
<header class="main-header">
|
||||
<a href="/admin/" class="logo">
|
||||
<span class="logo-lg">{{ config["SITENAME"] }}</span>
|
||||
<span class="logo-lg">Mailu</span>
|
||||
</a>
|
||||
</header>
|
||||
<aside class="main-sidebar">
|
||||
@@ -53,7 +53,22 @@ class="hold-transition skin-blue sidebar-mini"
|
||||
|
||||
<section class="content">
|
||||
{{ utils.flashed_messages(container=False) }}
|
||||
{% block content %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="box">
|
||||
{% block box %}
|
||||
<div class="box-header">
|
||||
{% block box_title %}{% endblock %}
|
||||
</div>
|
||||
<div class="box-body">
|
||||
{% block box_content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</section>
|
||||
</div>
|
||||
<footer class="main-footer">
|
||||
@@ -8,9 +8,7 @@
|
||||
{{ action }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.box(theme="warning") %}
|
||||
{% block box_content %}
|
||||
<p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p>
|
||||
{{ macros.form(form) }}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,7 @@
|
||||
{{ action }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block box_content %}
|
||||
<p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p>
|
||||
<pre>{{ error }}</pre>
|
||||
{% endblock %}
|
||||
@@ -4,8 +4,7 @@
|
||||
{% trans %}New domain{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.box() %}
|
||||
{% block box_content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.name) }}
|
||||
@@ -16,5 +15,4 @@
|
||||
{{ macros.form_field(form.comment) }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
50
admin/mailu/admin/templates/domain/details.html
Normal file
50
admin/mailu/admin/templates/domain/details.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Domain details{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{{ domain.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{% if current_user.global_admin %}
|
||||
<a class="btn btn-primary" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">{% trans %}Regenerate keys{% endtrans %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block box %}
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Domain name{% endtrans %}</th>
|
||||
<td>{{ domain.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}DNS MX entry{% endtrans %}</th>
|
||||
<td><pre>{{ domain.name }}. 600 IN MX 10 {{ config["HOSTNAME"] }}.</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}DNS SPF entries{% endtrans %}</th>
|
||||
<td><pre>
|
||||
{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"
|
||||
{{ domain.name }}. 600 IN SPF "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"</pre></td>
|
||||
</tr>
|
||||
{% if domain.dkim_publickey %}
|
||||
<tr>
|
||||
<th>{% trans %}DKIM public key{% endtrans %}</th>
|
||||
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ domain.dkim_publickey }}</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}DNS DKIM entry{% endtrans %}</th>
|
||||
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ config["DKIM_SELECTOR"] }}._domainkey.{{ domain.name }}. IN 600 TXT "v=DKIM1; k=rsa; p={{ domain.dkim_publickey }}"</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}DNS DMARC entry{% endtrans %}</th>
|
||||
<td><pre>_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:{{ config["POSTMASTER"] }}@{{ config["DOMAIN"] }}; adkim=s; aspf=s"</pre></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
50
admin/mailu/admin/templates/domain/list.html
Normal file
50
admin/mailu/admin/templates/domain/list.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Domain list{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{% if current_user.global_admin %}
|
||||
<a class="btn btn-primary" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block box %}
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
<th>{% trans %}Manage{% endtrans %}</th>
|
||||
<th>{% trans %}Domain name{% endtrans %}</th>
|
||||
<th>{% trans %}Mailbox count{% endtrans %}</th>
|
||||
<th>{% trans %}Alias count{% endtrans %}</th>
|
||||
<th>{% trans %}Comment{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
<th>{% trans %}Last edit{% endtrans %}</th>
|
||||
</tr>
|
||||
{% for domain in current_user.get_managed_domains() %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>
|
||||
{% if current_user.global_admin %}
|
||||
<a href="{{ url_for('.domain_edit', domain_name=domain.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>
|
||||
<a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="fa fa-envelope-o"></i></a>
|
||||
<a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>
|
||||
<a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>
|
||||
</td>
|
||||
<td>{{ domain.name }}</td>
|
||||
<td>{{ domain.users | count }} / {{ domain.max_users or '∞' }}</td>
|
||||
<td>{{ domain.aliases | count }} / {{ domain.max_aliases or '∞' }}</td>
|
||||
<td>{{ domain.comment or '' }}</td>
|
||||
<td>{{ domain.created_at }}</td>
|
||||
<td>{{ domain.updated_at or '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Create an authentication token{% endtrans %}
|
||||
{% trans %}Add a fetched account{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "fetch/create.html" %}
|
||||
{% extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Update a fetched account{% endtrans %}
|
||||
45
admin/mailu/admin/templates/fetch/list.html
Normal file
45
admin/mailu/admin/templates/fetch/list.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Fetched accounts{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
<a class="btn btn-primary" href="{{ url_for('.fetch_create', user_email=user.email) }}">{% trans %}Add an account{% endtrans %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block box %}
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
<th>{% trans %}Endpoint{% endtrans %}</th>
|
||||
<th>{% trans %}Username{% endtrans %}</th>
|
||||
<th>{% trans %}Keep emails{% endtrans %}</th>
|
||||
<th>{% trans %}Last check{% endtrans %}</th>
|
||||
<th>{% trans %}Status{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
<th>{% trans %}Last edit{% endtrans %}</th>
|
||||
</tr>
|
||||
{% for fetch in user.fetches %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>
|
||||
<a href="{{ url_for('.fetch_delete', fetch_id=fetch.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
|
||||
<td>{{ fetch.username }}</td>
|
||||
<td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
|
||||
<td>{{ fetch.last_check or '-' }}</td>
|
||||
<td>{{ fetch.error or '-' }}</td>
|
||||
<td>{{ fetch.created_at }}</td>
|
||||
<td>{{ fetch.updated_at or '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.box() %}
|
||||
{% block box_content %}
|
||||
{{ macros.form(form) }}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
13
admin/mailu/admin/templates/helpers.html
Normal file
13
admin/mailu/admin/templates/helpers.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% macro render_field(field, label_visible=true) -%}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
||||
{% if field.type != 'HiddenField' and label_visible %}
|
||||
<label for="{{ field.id }}" class="control-label">{{ field.label }}</label>
|
||||
{% endif %}
|
||||
{{ field(class_='form-control', **kwargs) }}
|
||||
{% if field.errors %}
|
||||
{% for e in field.errors %}
|
||||
<p class="help-block">{{ e }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
5
admin/mailu/admin/templates/index.html
Normal file
5
admin/mailu/admin/templates/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends "general.html" %}
|
||||
|
||||
{% block content %}
|
||||
Test
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user