1 Commits
1.5 ... 1.4

Author SHA1 Message Date
kaiyou
79529a4aac Add the dns tracking to the stable branch for the update to 1.5 2017-11-10 09:47:16 +01:00
305 changed files with 4119 additions and 11098 deletions

77
.env.dist Normal file
View 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

13
.gitignore vendored
View File

@@ -1,15 +1,12 @@
*.pyc *.pyc
*.mo *.mo
__pycache__ __pycache__
/admin/lib
/admin/bin
/admin/include
pip-selfcheck.json pip-selfcheck.json
/core/admin/lib*
/core/admin/bin
/core/admin/include
/docs/lib*
/docs/bin
/docs/include
/docs/_build
/.env /.env
/data
/docker-compose.mac.yml
/docker-compose.yml /docker-compose.yml
/.idea /.idea
/.vscode

View File

@@ -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+$

View File

@@ -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

View File

@@ -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 your name, pseudonym, and any contact information you feel is relevant to
this file. 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: Other contributors:
- "Angedestenebres" - Tests on development version & Current version - Angedestenebres - Tests on development version & Current version
- Stefan Auditor - German translation on POEditor.com - 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

View File

@@ -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 details about individual changes, see the Git log. You should read this before
upgrading Freposte.io as some changes will include useful notes. 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 v1.4.0 - 2017-02-12
------------------- -------------------

View File

@@ -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/

View File

@@ -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.

View File

@@ -1,15 +1,28 @@
![Logo](docs/assets/logo.png) ![Logo](logo.png)
Mailu is a simple yet full-featured mail server as a set of Docker images. [Join us and chat about the project.](https://riot.im/app/#/room/#mailu:tedomum.net)
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.
Most of the documentation is available on our [Website](https://mailu.io), Mailu
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).
*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 Features
======== ========
@@ -17,15 +30,28 @@ Features
Main features include: Main features include:
- **Standard email server**, IMAP and IMAP+, SMTP and Submission - **Standard email server**, IMAP and IMAP+, SMTP and Submission
- **Advanced email features**, aliases, domain aliases, custom routing
- **Web access**, multiple Webmails and adminitration interface - **Web access**, multiple Webmails and adminitration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts - **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas - **Admin features**, global admins, per-domain delegation, quotas
- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Security**, enforced TLS, outgoing DKIM, anti-virus scanner
- **Antispam**, auto-learn, greylisting, DMARC and SPF - **Antispam**, auto-learn, greylisting, DMARC and SPF
- **Freedom**, all FOSS components, no tracker included - **Freedom**, all FOSS components, no tracker included
![Domains](docs/assets/screenshots/domains.png) ![Creating a new user](https://mailu.io/screenshots/create.png)
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 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 components are free software and compatible with the MIT license. All
specific configuration files, Dockerfiles and code are placed under the specific configuration files, Dockerfiles and code are placed under the
MIT license. MIT license.
For details, see the [Contributor Guide](https://github.com/mail-u/mailu/wiki/Contributors-Guide).

16
admin/Dockerfile Normal file
View 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
View 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"))

View 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

View File

@@ -1,5 +1,4 @@
from mailu import db, models from mailu.admin import db, models, forms
from mailu.ui import forms
import flask import flask
import flask_login import flask_login

View File

@@ -51,18 +51,6 @@ class DomainForm(flask_wtf.FlaskForm):
submit = fields.SubmitField(_('Create')) 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): class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired()]) localpart = fields.StringField(_('E-mail'), [validators.DataRequired()])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
@@ -77,7 +65,7 @@ class UserForm(flask_wtf.FlaskForm):
class UserSettingsForm(flask_wtf.FlaskForm): class UserSettingsForm(flask_wtf.FlaskForm):
displayed_name = fields.StringField(_('Displayed name')) displayed_name = fields.StringField(_('Displayed name'))
spam_enabled = fields.BooleanField(_('Enable spam filter')) 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')) submit = fields.SubmitField(_('Save settings'))
@@ -89,7 +77,6 @@ class UserPasswordForm(flask_wtf.FlaskForm):
class UserForwardForm(flask_wtf.FlaskForm): class UserForwardForm(flask_wtf.FlaskForm):
forward_enabled = fields.BooleanField(_('Enable forwarding')) forward_enabled = fields.BooleanField(_('Enable forwarding'))
forward_keep = fields.BooleanField(_('Keep a copy of the emails'))
forward_destination = fields.StringField( forward_destination = fields.StringField(
_('Destination'), [validators.Optional(), validators.Email()] _('Destination'), [validators.Optional(), validators.Email()]
) )
@@ -101,22 +88,9 @@ class UserReplyForm(flask_wtf.FlaskForm):
reply_subject = fields.StringField(_('Reply subject')) reply_subject = fields.StringField(_('Reply subject'))
reply_body = fields.StringField(_('Reply body'), reply_body = fields.StringField(_('Reply body'),
widget=widgets.TextArea()) widget=widgets.TextArea())
reply_enddate = fields.html5.DateField(_('End of vacation'))
submit = fields.SubmitField(_('Update')) 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): class AliasForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Alias'), [validators.DataRequired()]) localpart = fields.StringField(_('Alias'), [validators.DataRequired()])
wildcard = fields.BooleanField( wildcard = fields.BooleanField(

View File

@@ -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 sqlalchemy.ext import declarative
from passlib import context, hash from passlib import context
from datetime import datetime, date from datetime import datetime
from email.mime import text
import re import re
import time import time
import os import os
import glob import glob
import smtplib
# Many-to-many association table for domain managers # Many-to-many association table for domain managers
@@ -102,36 +100,6 @@ class Domain(Base):
return False 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): class Email(object):
""" Abstraction for an email address (localpart and domain). """ Abstraction for an email address (localpart and domain).
""" """
@@ -152,22 +120,10 @@ class Email(object):
context.current_parameters["localpart"], context.current_parameters["localpart"],
context.current_parameters["domain_name"], context.current_parameters["domain_name"],
) )
return db.Column(db.String(255, collation="NOCASE"), return db.Column(db.String(255),
primary_key=True, nullable=False, primary_key=True, nullable=False,
default=updater) 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): def __str__(self):
return self.email return self.email
@@ -190,12 +146,9 @@ class User(Base, Email):
# Filters # Filters
forward_enabled = db.Column(db.Boolean(), nullable=False, default=False) forward_enabled = db.Column(db.Boolean(), nullable=False, default=False)
forward_destination = db.Column(db.String(255), nullable=True, default=None) 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_enabled = db.Column(db.Boolean(), nullable=False, default=False)
reply_subject = db.Column(db.String(255), nullable=True, default=None) reply_subject = db.Column(db.String(255), nullable=True, default=None)
reply_body = db.Column(db.Text(), 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 # Settings
displayed_name = db.Column(db.String(160), nullable=False, default="") displayed_name = db.Column(db.String(160), nullable=False, default="")
@@ -210,28 +163,16 @@ class User(Base, Email):
def get_id(self): def get_id(self):
return self.email return self.email
scheme_dict = {'SHA512-CRYPT': "sha512_crypt",
'SHA256-CRYPT': "sha256_crypt",
'MD5-CRYPT': "md5_crypt",
'CRYPT': "des_crypt"}
pw_context = context.CryptContext( pw_context = context.CryptContext(
schemes = scheme_dict.values(), ["sha512_crypt", "sha256_crypt", "md5_crypt"]
default=scheme_dict[app.config['PASSWORD_SCHEME']],
) )
def check_password(self, password): def check_password(self, password):
reference = re.match('({[^}]+})?(.*)', self.password).group(2) reference = re.match('({[^}]+})?(.*)', self.password).group(2)
return User.pw_context.verify(password, reference) return User.pw_context.verify(password, reference)
def set_password(self, password, hash_scheme=app.config['PASSWORD_SCHEME'], raw=False): def set_password(self, password):
"""Set password for user with specified encryption scheme self.password = '{SHA512-CRYPT}' + User.pw_context.encrypt(password)
@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 get_managed_domains(self): def get_managed_domains(self):
if self.global_admin: if self.global_admin:
@@ -247,18 +188,11 @@ class User(Base, Email):
emails.extend(domain.aliases) emails.extend(domain.aliases)
return emails return emails
def send_welcome(self):
if app.config["WELCOME"].lower() == "true":
self.sendmail(app.config["WELCOME_SUBJECT"],
app.config["WELCOME_BODY"])
@classmethod @classmethod
def login(cls, email, password): def login(cls, email, password):
user = cls.query.get(email) user = cls.query.get(email)
return user if (user and user.check_password(password)) else None return user if (user and user.check_password(password)) else None
login_manager.user_loader(User.query.get)
class Alias(Base, Email): class Alias(Base, Email):
""" An alias is an email address that redirects to some destination. """ 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=[]) 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): class Fetch(Base):
""" A fetched account is a repote POP/IMAP account fetched into a local """ A fetched account is a repote POP/IMAP account fetched into a local
account. account.

View File

@@ -4,8 +4,7 @@
{% trans %}Add a global administrator{% endtrans %} {% trans %}Add a global administrator{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block box_content %}
{% call macros.box() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.admin, id='admin') }} {{ macros.form_field(form.admin, id='admin') }}
@@ -14,5 +13,4 @@
$("#admin").select2(); $("#admin").select2();
</script> </script>
</form> </form>
{% endcall %}
{% endblock %} {% endblock %}

View 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 %}

View File

@@ -8,8 +8,7 @@
{{ domain }} {{ domain }}
{% endblock %} {% endblock %}
{% block content %} {% block box_content %}
{% call macros.box() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-addon">@'+domain.name+'</span>') }}
@@ -24,5 +23,4 @@
}) })
</script> </script>
</form> </form>
{% endcall %}
{% endblock %} {% endblock %}

View 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>&nbsp;
<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 %}

View File

@@ -4,13 +4,15 @@
{% trans %}Public announcement{% endtrans %} {% trans %}Public announcement{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block subtitle %}
{% call macros.box() %} {% trans %}from{% endtrans %} {{ from_address }}
{% endblock %}
{% block box_content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.announcement_subject) }} {{ macros.form_field(form.announcement_subject) }}
{{ macros.form_field(form.announcement_body, rows=10) }} {{ macros.form_field(form.announcement_body, rows=10) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %}
{% endblock %} {% endblock %}

View File

@@ -29,7 +29,7 @@ class="hold-transition skin-blue sidebar-mini"
{% block navbar %} {% block navbar %}
<header class="main-header"> <header class="main-header">
<a href="/admin/" class="logo"> <a href="/admin/" class="logo">
<span class="logo-lg">{{ config["SITENAME"] }}</span> <span class="logo-lg">Mailu</span>
</a> </a>
</header> </header>
<aside class="main-sidebar"> <aside class="main-sidebar">
@@ -53,7 +53,22 @@ class="hold-transition skin-blue sidebar-mini"
<section class="content"> <section class="content">
{{ utils.flashed_messages(container=False) }} {{ 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> </section>
</div> </div>
<footer class="main-footer"> <footer class="main-footer">

View File

@@ -8,9 +8,7 @@
{{ action }} {{ action }}
{% endblock %} {% endblock %}
{% block content %} {% block box_content %}
{% call macros.box(theme="warning") %}
<p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p> <p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p>
{{ macros.form(form) }} {{ macros.form(form) }}
{% endcall %}
{% endblock %} {% endblock %}

View File

@@ -8,7 +8,7 @@
{{ action }} {{ action }}
{% endblock %} {% endblock %}
{% block content %} {% block box_content %}
<p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p> <p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p>
<pre>{{ error }}</pre> <pre>{{ error }}</pre>
{% endblock %} {% endblock %}

View File

@@ -4,8 +4,7 @@
{% trans %}New domain{% endtrans %} {% trans %}New domain{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block box_content %}
{% call macros.box() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.name) }} {{ macros.form_field(form.name) }}
@@ -16,5 +15,4 @@
{{ macros.form_field(form.comment) }} {{ macros.form_field(form.comment) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %}
{% endblock %} {% endblock %}

View 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 %}

View 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>&nbsp;
{% 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>&nbsp;
<a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
{% 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>&nbsp;
<a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>&nbsp;
<a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>&nbsp;
</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 %}

View File

@@ -1,7 +1,7 @@
{% extends "form.html" %} {% extends "form.html" %}
{% block title %} {% block title %}
{% trans %}Create an authentication token{% endtrans %} {% trans %}Add a fetched account{% endtrans %}
{% endblock %} {% endblock %}
{% block subtitle %} {% block subtitle %}

View File

@@ -1,4 +1,4 @@
{% extends "fetch/create.html" %} {% extends "form.html" %}
{% block title %} {% block title %}
{% trans %}Update a fetched account{% endtrans %} {% trans %}Update a fetched account{% endtrans %}

View 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>&nbsp;
<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 %}

View File

@@ -1,7 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block box_content %}
{% call macros.box() %}
{{ macros.form(form) }} {{ macros.form(form) }}
{% endcall %}
{% endblock %} {% endblock %}

View 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 %}

View 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