mirror of
https://github.com/optim-enterprises-bv/Mailu.git
synced 2025-11-01 18:47:52 +00:00
Ratelimit outgoing emails per user
This commit is contained in:
@@ -46,6 +46,7 @@ DEFAULT_CONFIG = {
|
||||
'DKIM_SELECTOR': 'dkim',
|
||||
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
|
||||
'DEFAULT_QUOTA': 1000000000,
|
||||
'MESSAGE_RATELIMIT': '100/hour',
|
||||
# Web settings
|
||||
'SITENAME': 'Mailu',
|
||||
'WEBSITE': 'https://mailu.io',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from mailu import models
|
||||
from mailu import models, utils
|
||||
from mailu.internal import internal
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
import idna
|
||||
@@ -31,7 +32,6 @@ def postfix_alias_map(alias):
|
||||
destination = models.Email.resolve_destination(localpart, domain_name)
|
||||
return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
|
||||
|
||||
|
||||
@internal.route("/postfix/transport/<path:email>")
|
||||
def postfix_transport(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)
|
||||
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>")
|
||||
def postfix_sender_access(sender):
|
||||
|
||||
@@ -27,7 +27,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.inspection import inspect
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from mailu import dkim
|
||||
from mailu import dkim, utils
|
||||
|
||||
|
||||
db = flask_sqlalchemy.SQLAlchemy()
|
||||
@@ -501,6 +501,12 @@ class User(Base, Email):
|
||||
self.reply_enddate > now
|
||||
)
|
||||
|
||||
@property
|
||||
def sender_limiter(self):
|
||||
return utils.limiter.get_limiter(
|
||||
app.config["MESSAGE_RATELIMIT"], "sender", self.email
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_password_context(cls):
|
||||
""" create password context for hashing and verification
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<th>{% trans %}User settings{% endtrans %}</th>
|
||||
<th>{% trans %}Email{% 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 %}Created{% endtrans %}</th>
|
||||
<th>{% trans %}Last edit{% endtrans %}</th>
|
||||
@@ -41,6 +42,8 @@
|
||||
{% if user.enable_pop %}<span class="label label-info">pop3</span>{% endif %}
|
||||
</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.created_at }}</td>
|
||||
<td>{{ user.updated_at or '' }}</td>
|
||||
|
||||
@@ -100,6 +100,7 @@ smtpd_sender_login_maps = ${podop}senderlogin
|
||||
smtpd_helo_required = yes
|
||||
|
||||
smtpd_client_restrictions =
|
||||
check_sasl_access ${podop}senderrate,
|
||||
permit_mynetworks,
|
||||
check_sender_access ${podop}senderaccess,
|
||||
reject_non_fqdn_sender,
|
||||
|
||||
@@ -25,7 +25,8 @@ def start_podop():
|
||||
("recipientmap", "url", url + "recipient/map/§"),
|
||||
("sendermap", "url", url + "sender/map/§"),
|
||||
("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):
|
||||
|
||||
@@ -62,6 +62,11 @@ ANTIVIRUS={{ antivirus_enabled or 'none' }}
|
||||
# Max attachment size will be 33% smaller
|
||||
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
|
||||
# Use this with care, all hosts in this networks will be able to send mail without authentication!
|
||||
RELAYNETS=
|
||||
|
||||
@@ -55,6 +55,13 @@ Or in plain english: if receivers start to classify your mail as spam, this post
|
||||
</p>
|
||||
</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">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" name="disable_statistics" value="True">
|
||||
|
||||
1
towncrier/newsfragments/1031.feature
Normal file
1
towncrier/newsfragments/1031.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add sending quotas per user
|
||||
Reference in New Issue
Block a user