You've already forked Mailu
mirror of
https://github.com/Mailu/Mailu.git
synced 2025-11-23 22:04:47 +02:00
Merge #1916
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>
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': '200/day',
|
||||
# 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("450 4.2.1 You are sending too many emails too fast.")
|
||||
|
||||
@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>
|
||||
|
||||
@@ -101,6 +101,8 @@ smtpd_sender_login_maps = ${podop}senderlogin
|
||||
# Restrictions for incoming SMTP, other restrictions are applied in master.cf
|
||||
smtpd_helo_required = yes
|
||||
|
||||
check_ratelimit = check_sasl_access ${podop}senderrate
|
||||
|
||||
smtpd_client_restrictions =
|
||||
permit_mynetworks,
|
||||
check_sender_access ${podop}senderaccess,
|
||||
|
||||
@@ -7,7 +7,8 @@ smtp inet n - n - - smtpd
|
||||
# Internal SMTP service
|
||||
10025 inet n - n - - smtpd
|
||||
-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 cleanup_service_name=outclean
|
||||
outclean unix n - n - 0 cleanup
|
||||
|
||||
@@ -26,7 +26,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):
|
||||
|
||||
Reference in New Issue
Block a user