Ratelimit outgoing emails per user

master
Florent Daigniere 3 years ago
parent 6b0e8a0dfb
commit 1438253a06

@ -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': '100/hour',
# 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("REJECT")
@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>

@ -100,6 +100,7 @@ smtpd_sender_login_maps = ${podop}senderlogin
smtpd_helo_required = yes smtpd_helo_required = yes
smtpd_client_restrictions = smtpd_client_restrictions =
check_sasl_access ${podop}senderrate,
permit_mynetworks, permit_mynetworks,
check_sender_access ${podop}senderaccess, check_sender_access ${podop}senderaccess,
reject_non_fqdn_sender, reject_non_fqdn_sender,

@ -25,7 +25,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):

@ -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="100" 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