mirror of
https://github.com/optim-enterprises-bv/Mailu.git
synced 2025-11-02 19:18:07 +00:00
Ratelimit outgoing emails per user
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
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