1 Commits
master ... 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
436 changed files with 5132 additions and 23543 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

15
.gitignore vendored
View File

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

View File

@@ -1,25 +0,0 @@
pull_request_rules:
- name: Successful travis and 2 approved reviews
conditions:
- status-success=continuous-integration/travis-ci/pr
- label!=["status"/wip","status/blocked"]
- "#approved-reviews-by>=2"
actions:
merge:
method: merge
strict: true
dismiss_reviews:
approved: true
- name: Trusted author, successful travis and 1 approved review
conditions:
- author~=(kaiyou|muhlemmer|mildred|HorayNarea|adi90x|hoellen|ofthesun9)
- status-success=continuous-integration/travis-ci/pr
- label!=["status"/wip","status/blocked","review/need2"]
- "#approved-reviews-by>=1"
actions:
merge:
method: merge
strict: true
dismiss_reviews:
approved: true

View File

@@ -1,37 +0,0 @@
sudo: required
services: docker
addons:
apt:
packages:
- docker-ce
env:
- MAILU_VERSION=$TRAVIS_BRANCH
language: python
python:
- "3.6"
install:
- pip install -r tests/requirements.txt
- 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
- sudo -- sh -c 'mkdir -p /mailu && cp -r tests/certs /mailu && chmod 600 /mailu/certs/*'
script:
# test.py, test name and timeout between start and tests.
- python tests/compose/test.py core 1
- python tests/compose/test.py fetchmail 1
- travis_wait python tests/compose/test.py filters 10
- python tests/compose/test.py rainloop 1
- python tests/compose/test.py roundcube 1
- python tests/compose/test.py webdav 1
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
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

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

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,16 +1,28 @@
<p align="leftr"><img src="docs/assets/logomark.png" alt="Mailu" height="200px"></p>
![Logo](logo.png)
[Join us and chat about the project.](https://riot.im/app/#/room/#mailu:tedomum.net)
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.
Mailu
=====
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).
*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
========
@@ -18,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 administration interface
- **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
![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
============
@@ -35,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
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 models
from mailu.ui import forms
from mailu.admin import db, models, forms
import flask
import flask_login

View File

@@ -6,7 +6,6 @@ import flask_login
import flask_wtf
import re
LOCALPART_REGEX = "^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+$"
class DestinationField(fields.SelectMultipleField):
""" Allow for multiple emails selection from current user choices and
@@ -48,62 +47,25 @@ class DomainForm(flask_wtf.FlaskForm):
max_users = fields_.IntegerField(_('Maximum user count'), default=10)
max_aliases = fields_.IntegerField(_('Maximum alias count'), default=10)
max_quota_bytes = fields_.IntegerSliderField(_('Maximum user quota'), default=0)
signup_enabled = fields.BooleanField(_('Enable sign-up'), default=False)
comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Save'))
class DomainSignupForm(flask_wtf.FlaskForm):
name = fields.StringField(_('Domain name'), [validators.DataRequired()])
localpart = fields.StringField(_('Initial admin'), [validators.DataRequired()])
pw = fields.PasswordField(_('Admin password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
captcha = flask_wtf.RecaptchaField()
submit = fields.SubmitField(_('Create'))
class AlternativeForm(flask_wtf.FlaskForm):
name = fields.StringField(_('Alternative name'), [validators.DataRequired()])
submit = fields.SubmitField(_('Save'))
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(_('Save'))
class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
localpart = fields.StringField(_('E-mail'), [validators.DataRequired()])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000)
enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True)
comment = fields.StringField(_('Comment'))
enabled = fields.BooleanField(_('Enabled'), default=True)
submit = fields.SubmitField(_('Save'))
class UserSignupForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
submit = fields.SubmitField(_('Sign up'))
class UserSignupFormCaptcha(UserSignupForm):
captcha = flask_wtf.RecaptchaField()
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'))
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()]
)
spam_threshold = fields_.IntegerSliderField(_('Spam filter threshold'))
submit = fields.SubmitField(_('Save settings'))
@@ -113,35 +75,29 @@ class UserPasswordForm(flask_wtf.FlaskForm):
submit = fields.SubmitField(_('Update password'))
class UserForwardForm(flask_wtf.FlaskForm):
forward_enabled = fields.BooleanField(_('Enable forwarding'))
forward_destination = fields.StringField(
_('Destination'), [validators.Optional(), validators.Email()]
)
submit = fields.SubmitField(_('Update'))
class UserReplyForm(flask_wtf.FlaskForm):
reply_enabled = fields.BooleanField(_('Enable automatic reply'))
reply_subject = fields.StringField(_('Reply subject'))
reply_body = fields.StringField(_('Reply body'),
widget=widgets.TextArea())
reply_startdate = fields.html5.DateField(_('Start of vacation'))
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(_('Save'))
class AliasForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Alias'), [validators.DataRequired()])
wildcard = fields.BooleanField(
_('Use SQL LIKE Syntax (e.g. for catch-all aliases)'))
destination = DestinationField(_('Destination'))
comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Save'))
submit = fields.SubmitField(_('Create'))
class AdminForm(flask_wtf.FlaskForm):

227
admin/mailu/admin/models.py Normal file
View File

@@ -0,0 +1,227 @@
from mailu.admin import db, dkim
from mailu import app
from sqlalchemy.ext import declarative
from passlib import context
from datetime import datetime
import re
import time
import os
import glob
# Many-to-many association table for domain managers
managers = db.Table('manager',
db.Column('domain_name', db.String(80), db.ForeignKey('domain.name')),
db.Column('user_email', db.String(255), db.ForeignKey('user.email'))
)
class CommaSeparatedList(db.TypeDecorator):
""" Stores a list as a comma-separated string, compatible with Postfix.
"""
impl = db.String
def process_bind_param(self, value, dialect):
if type(value) is not list:
raise TypeError("Shoud be a list")
for item in value:
if "," in item:
raise ValueError("No item should contain a comma")
return ",".join(value)
def process_result_value(self, value, dialect):
return filter(bool, value.split(","))
class Base(db.Model):
""" Base class for all models
"""
__abstract__ = True
created_at = db.Column(db.Date, nullable=False, default=datetime.now)
updated_at = db.Column(db.Date, nullable=True, onupdate=datetime.now)
comment = db.Column(db.String(255), nullable=True)
class Domain(Base):
""" A DNS domain that has mail addresses associated to it.
"""
__tablename__ = "domain"
name = db.Column(db.String(80), primary_key=True, nullable=False)
managers = db.relationship('User', secondary=managers,
backref=db.backref('manager_of'), lazy='dynamic')
max_users = db.Column(db.Integer, nullable=False, default=0)
max_aliases = db.Column(db.Integer, nullable=False, default=0)
max_quota_bytes = db.Column(db.Integer(), nullable=False, default=0)
@property
def dkim_key(self):
file_path = app.config["DKIM_PATH"].format(
domain=self.name, selector=app.config["DKIM_SELECTOR"])
if os.path.exists(file_path):
with open(file_path, "rb") as handle:
return handle.read()
@dkim_key.setter
def dkim_key(self, value):
file_path = app.config["DKIM_PATH"].format(
domain=self.name, selector=app.config["DKIM_SELECTOR"])
with open(file_path, "wb") as handle:
handle.write(value)
@property
def dkim_publickey(self):
dkim_key = self.dkim_key
if dkim_key:
return dkim.strip_key(self.dkim_key).decode("utf8")
def generate_dkim_key(self):
self.dkim_key = dkim.gen_key()
def has_email(self, localpart):
for email in self.users + self.aliases:
if email.localpart == localpart:
return True
else:
return False
def __str__(self):
return self.name
def __eq__(self, other):
try:
return self.name == other.name
except AttributeError:
return False
class Email(object):
""" Abstraction for an email address (localpart and domain).
"""
localpart = db.Column(db.String(80), nullable=False)
@declarative.declared_attr
def domain_name(cls):
return db.Column(db.String(80), db.ForeignKey(Domain.name),
nullable=False)
# This field is redundant with both localpart and domain name.
# It is however very useful for quick lookups without joining tables,
# especially when the mail server il reading the database.
@declarative.declared_attr
def email(cls):
updater = lambda context: "{0}@{1}".format(
context.current_parameters["localpart"],
context.current_parameters["domain_name"],
)
return db.Column(db.String(255),
primary_key=True, nullable=False,
default=updater)
def __str__(self):
return self.email
class User(Base, Email):
""" A user is an email address that has a password to access a mailbox.
"""
__tablename__ = "user"
domain = db.relationship(Domain,
backref=db.backref('users', cascade='all, delete-orphan'))
password = db.Column(db.String(255), nullable=False)
quota_bytes = db.Column(db.Integer(), nullable=False, default=10**9)
global_admin = db.Column(db.Boolean(), nullable=False, default=False)
# Features
enable_imap = db.Column(db.Boolean(), nullable=False, default=True)
enable_pop = db.Column(db.Boolean(), nullable=False, default=True)
# Filters
forward_enabled = db.Column(db.Boolean(), nullable=False, default=False)
forward_destination = db.Column(db.String(255), nullable=True, default=None)
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)
# Settings
displayed_name = db.Column(db.String(160), nullable=False, default="")
spam_enabled = db.Column(db.Boolean(), nullable=False, default=True)
spam_threshold = db.Column(db.Integer(), nullable=False, default=80.0)
# Flask-login attributes
is_authenticated = True
is_active = True
is_anonymous = False
def get_id(self):
return self.email
pw_context = context.CryptContext(
["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):
self.password = '{SHA512-CRYPT}' + User.pw_context.encrypt(password)
def get_managed_domains(self):
if self.global_admin:
return Domain.query.all()
else:
return self.manager_of
def get_managed_emails(self, include_aliases=True):
emails = []
for domain in self.get_managed_domains():
emails.extend(domain.users)
if include_aliases:
emails.extend(domain.aliases)
return emails
@classmethod
def login(cls, email, password):
user = cls.query.get(email)
return user if (user and user.check_password(password)) else None
class Alias(Base, Email):
""" An alias is an email address that redirects to some destination.
"""
__tablename__ = "alias"
domain = db.relationship(Domain,
backref=db.backref('aliases', cascade='all, delete-orphan'))
wildcard = db.Column(db.Boolean(), nullable=False, default=False)
destination = db.Column(CommaSeparatedList, nullable=False, default=[])
class Fetch(Base):
""" A fetched account is a repote POP/IMAP account fetched into a local
account.
"""
__tablename__ = "fetch"
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('fetches', cascade='all, delete-orphan'))
protocol = db.Column(db.Enum('imap', 'pop3'), nullable=False)
host = db.Column(db.String(255), nullable=False)
port = db.Column(db.Integer(), nullable=False)
tls = db.Column(db.Boolean(), nullable=False)
username = db.Column(db.String(255), nullable=False)
password = db.Column(db.String(255), nullable=False)
keep = db.Column(db.Boolean(), nullable=False)
last_check = db.Column(db.DateTime, nullable=True)
error = db.Column(db.String(1023), nullable=True)

View File

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

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

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

View File

@@ -5,7 +5,7 @@
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{ url_for('.static', filename='select2/css/select2.min.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='fontawesome/css/font-awesome.min.css') }}">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link rel="stylesheet" href="{{ url_for('.static', filename='adminlte/css/AdminLTE.min.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='adminlte/css/skin-blue.min.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='app.css') }}">
@@ -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">

View File

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

View File

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

View File

@@ -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) }}
@@ -13,9 +12,7 @@
{{ macros.form_field(form.max_quota_bytes, step=1000000000, max=50000000000,
prepend='<span class="input-group-addon"><span id="quota">'+((form.max_quota_bytes.data//1000000000).__str__() if form.max_quota_bytes.data else '∞')+'</span> GiB</span>',
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.signup_enabled) }}
{{ macros.form_field(form.comment) }}
{{ macros.form_field(form.submit) }}
</form>
{% endcall %}
{% 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" %}
{% block title %}
{% trans %}Create an authentication token{% endtrans %}
{% trans %}Add a fetched account{% endtrans %}
{% endblock %}
{% block subtitle %}

View File

@@ -1,4 +1,4 @@
{% extends "fetch/create.html" %}
{% extends "form.html" %}
{% block title %}
{% 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" %}
{% block content %}
{% call macros.box() %}
{% block box_content %}
{{ macros.form(form) }}
{% endcall %}
{% 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