1916: Ratelimit outgoing emails per user r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

A conflict-free version of #1360 implementing per-user sender limits

### Related issue(s)
- close #1360 
- close #1031
- close #1774 

## Prerequistes
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
master
bors[bot] 3 years ago committed by GitHub
commit b57df78dac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -46,6 +46,7 @@ DEFAULT_CONFIG = {
'DKIM_SELECTOR': 'dkim', 'DKIM_SELECTOR': 'dkim',
'DKIM_PATH': '/dkim/{domain}.{selector}.key', 'DKIM_PATH': '/dkim/{domain}.{selector}.key',
'DEFAULT_QUOTA': 1000000000, 'DEFAULT_QUOTA': 1000000000,
'MESSAGE_RATELIMIT': '200/day',
# Web settings # Web settings
'SITENAME': 'Mailu', 'SITENAME': 'Mailu',
'WEBSITE': 'https://mailu.io', 'WEBSITE': 'https://mailu.io',

@ -1,5 +1,6 @@
from mailu import models from mailu import models, utils
from mailu.internal import internal from mailu.internal import internal
from flask import current_app as app
import flask import flask
import idna import idna
@ -31,7 +32,6 @@ def postfix_alias_map(alias):
destination = models.Email.resolve_destination(localpart, domain_name) destination = models.Email.resolve_destination(localpart, domain_name)
return flask.jsonify(",".join(destination)) if destination else flask.abort(404) return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/transport/<path:email>") @internal.route("/postfix/transport/<path:email>")
def postfix_transport(email): def postfix_transport(email):
if email == '*' or re.match("(^|.*@)\[.*\]$", email): if email == '*' or re.match("(^|.*@)\[.*\]$", email):
@ -139,6 +139,12 @@ def postfix_sender_login(sender):
destination = models.Email.resolve_destination(localpart, domain_name, True) destination = models.Email.resolve_destination(localpart, domain_name, True)
return flask.jsonify(",".join(destination)) if destination else flask.abort(404) return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
@internal.route("/postfix/sender/rate/<path:sender>")
def postfix_sender_rate(sender):
""" Rate limit outbound emails per sender login
"""
user = models.User.get(sender) or flask.abort(404)
return flask.abort(404) if user.sender_limiter.hit() else flask.jsonify("450 4.2.1 You are sending too many emails too fast.")
@internal.route("/postfix/sender/access/<path:sender>") @internal.route("/postfix/sender/access/<path:sender>")
def postfix_sender_access(sender): def postfix_sender_access(sender):

@ -27,7 +27,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.inspection import inspect from sqlalchemy.inspection import inspect
from werkzeug.utils import cached_property from werkzeug.utils import cached_property
from mailu import dkim from mailu import dkim, utils
db = flask_sqlalchemy.SQLAlchemy() db = flask_sqlalchemy.SQLAlchemy()
@ -501,6 +501,12 @@ class User(Base, Email):
self.reply_enddate > now self.reply_enddate > now
) )
@property
def sender_limiter(self):
return utils.limiter.get_limiter(
app.config["MESSAGE_RATELIMIT"], "sender", self.email
)
@classmethod @classmethod
def get_password_context(cls): def get_password_context(cls):
""" create password context for hashing and verification """ create password context for hashing and verification

@ -19,7 +19,8 @@
<th>{% trans %}User settings{% endtrans %}</th> <th>{% trans %}User settings{% endtrans %}</th>
<th>{% trans %}Email{% endtrans %}</th> <th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Features{% endtrans %}</th> <th>{% trans %}Features{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th> <th>{% trans %}Storage Quota{% endtrans %}</th>
<th>{% trans %}Sending Quota{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th> <th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th> <th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Last edit{% endtrans %}</th> <th>{% trans %}Last edit{% endtrans %}</th>
@ -41,6 +42,8 @@
{% if user.enable_pop %}<span class="label label-info">pop3</span>{% endif %} {% if user.enable_pop %}<span class="label label-info">pop3</span>{% endif %}
</td> </td>
<td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td> <td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
{% set limiter = user.sender_limiter %}
<td>{{ limiter.get_window_stats()[1] }} / {{ limiter.limit }}</td>
<td>{{ user.comment or '-' }}</td> <td>{{ user.comment or '-' }}</td>
<td>{{ user.created_at }}</td> <td>{{ user.created_at }}</td>
<td>{{ user.updated_at or '' }}</td> <td>{{ user.updated_at or '' }}</td>

@ -101,6 +101,8 @@ smtpd_sender_login_maps = ${podop}senderlogin
# Restrictions for incoming SMTP, other restrictions are applied in master.cf # Restrictions for incoming SMTP, other restrictions are applied in master.cf
smtpd_helo_required = yes smtpd_helo_required = yes
check_ratelimit = check_sasl_access ${podop}senderrate
smtpd_client_restrictions = smtpd_client_restrictions =
permit_mynetworks, permit_mynetworks,
check_sender_access ${podop}senderaccess, check_sender_access ${podop}senderaccess,

@ -7,7 +7,8 @@ smtp inet n - n - - smtpd
# Internal SMTP service # Internal SMTP service
10025 inet n - n - - smtpd 10025 inet n - n - - smtpd
-o smtpd_sasl_auth_enable=yes -o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit -o smtpd_discard_ehlo_keywords=pipelining
-o smtpd_client_restrictions=$check_ratelimit,reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
-o smtpd_reject_unlisted_recipient={% if REJECT_UNLISTED_RECIPIENT %}{{ REJECT_UNLISTED_RECIPIENT }}{% else %}no{% endif %} -o smtpd_reject_unlisted_recipient={% if REJECT_UNLISTED_RECIPIENT %}{{ REJECT_UNLISTED_RECIPIENT }}{% else %}no{% endif %}
-o cleanup_service_name=outclean -o cleanup_service_name=outclean
outclean unix n - n - 0 cleanup outclean unix n - n - 0 cleanup

@ -26,7 +26,8 @@ def start_podop():
("recipientmap", "url", url + "recipient/map/§"), ("recipientmap", "url", url + "recipient/map/§"),
("sendermap", "url", url + "sender/map/§"), ("sendermap", "url", url + "sender/map/§"),
("senderaccess", "url", url + "sender/access/§"), ("senderaccess", "url", url + "sender/access/§"),
("senderlogin", "url", url + "sender/login/§") ("senderlogin", "url", url + "sender/login/§"),
("senderrate", "url", url + "sender/rate/§")
]) ])
def is_valid_postconf_line(line): def is_valid_postconf_line(line):

@ -315,6 +315,21 @@ This page is also accessible for domain managers. On the users page new users ca
* Fetched accounts. Access the fetched accounts page of the user. See the :ref:`fetched accounts page <webadministration_fetched_accounts>` for more information. * Fetched accounts. Access the fetched accounts page of the user. See the :ref:`fetched accounts page <webadministration_fetched_accounts>` for more information.
This page also shows an overview of the following settings of an user:
* Email. The email address of the user.
* Features. Shows if IMAP or POP3 access is enabled.
* Storage quota. Shows how much assigned storage has been consumed.
* Sending Quota. The sending quota is the limit of messages a single user can send per day.
* Comment. A desription for the user.
* Created. Date when the user was created.
* Last edit. Last date when the user was modified.
.. _webadministration_add_user: .. _webadministration_add_user:
@ -334,7 +349,7 @@ For adding a new user the following options can be configured.
* Enabled. Tick this checkbox to enable the user account. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail. * Enabled. Tick this checkbox to enable the user account. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail.
The email inbox of the user is still retained. This option can be used to temporarily suspend an user account. The email inbox of the user is still retained. This option can be used to temporarily suspend an user account.
* Quota. The maximum quota for the user's email box. * Storage Quota. The maximum quota for the user's email box.
* Allow IMAP access. When ticked, allows email retrieval via the IMAP protocol. * Allow IMAP access. When ticked, allows email retrieval via the IMAP protocol.

@ -62,6 +62,11 @@ ANTIVIRUS={{ antivirus_enabled or 'none' }}
# Max attachment size will be 33% smaller # Max attachment size will be 33% smaller
MESSAGE_SIZE_LIMIT={{ message_size_limit or '50000000' }} MESSAGE_SIZE_LIMIT={{ message_size_limit or '50000000' }}
# Message rate limit (per user)
{% if message_ratelimit_pd > '0' %}
MESSAGE_RATELIMIT={{ message_ratelimit_pd }}/day
{% endif %}
# Networks granted relay permissions # Networks granted relay permissions
# Use this with care, all hosts in this networks will be able to send mail without authentication! # Use this with care, all hosts in this networks will be able to send mail without authentication!
RELAYNETS= RELAYNETS=

@ -55,6 +55,13 @@ Or in plain english: if receivers start to classify your mail as spam, this post
</p> </p>
</div> </div>
<div class="form-group">
<label>Outgoing message rate limit (per user)</label>
<!-- Validates number input only -->
<p><input class="form-control" style="width: 7%; display: inline;" type="number" name="message_ratelimit_pd" value="200" required > / day
</p>
</div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> <label class="form-check-label">
<input class="form-check-input" type="checkbox" name="disable_statistics" value="True"> <input class="form-check-input" type="checkbox" name="disable_statistics" value="True">

@ -0,0 +1 @@
Add sending quotas per user
Loading…
Cancel
Save