-
-
Notifications
You must be signed in to change notification settings - Fork 609
[Feature] Introduce sender-blacklists (on both global/admin and user level) #2694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chrisblech
wants to merge
20
commits into
simple-login:master
Choose a base branch
from
chrisblech:feature/user-blacklists
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
cc17c20
Add global sender blacklist (regex) with admin UI and SMTP blocking
lio-chrisblech 90bd0cb
Blacklist: only apply when no Contact exists; create disabled contact
lio-chrisblech 2b8f535
Refactor contact creation + add pattern help text
lio-chrisblech e298007
Add per-user sender blacklist
lio-chrisblech c39dca2
DB: add user_id to global_sender_blacklist
lio-chrisblech 96c1fbc
Fix sender blacklist UI text, show global entries, and make migration…
lio-chrisblech cfdbbfb
cleanup, only show active global entries
chrisblech 1aeab12
Rename GlobalSenderBlacklist model to ForbiddenEnvelopeSender
lio-chrisblech 0011360
Admin: show all forbidden envelope sender entries
lio-chrisblech 203f8dd
Sender blacklist: cache patterns 5min, use fullmatch and explicit types
lio-chrisblech 2ca7a77
Email handler: apply sender blacklist before creating contacts
lio-chrisblech 851f58e
Dashboard: comment, validation and audit log for sender blacklist
lio-chrisblech 28554a8
Deps: add cachetools
lio-chrisblech 1f6496e
Tests: update model name for sender blacklist
lio-chrisblech 43173d1
Adjust sender blacklist validation, contact precedence, and cache sizing
lio-chrisblech 32223c3
Merge pull request #3 from chrisblech/feature/user-blacklists-review-…
chrisblech 748227c
Merge branch 'master' into feature/user-blacklists
chrisblech 60979ba
Merge branch 'master' into feature/user-blacklists
chrisblech bfb269a
reorder migrations
chrisblech a79100b
Merge branch 'master' into feature/user-blacklists
chrisblech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| from flask_admin.form import SecureForm | ||
|
|
||
| from app.admin.base import SLModelView | ||
|
|
||
|
|
||
| class GlobalSenderBlacklistAdmin(SLModelView): | ||
| form_base_class = SecureForm | ||
|
|
||
| can_create = True | ||
| can_edit = True | ||
| can_delete = True | ||
|
|
||
| column_searchable_list = ("pattern", "comment") | ||
| column_filters = ("enabled",) | ||
| column_editable_list = ("enabled", "comment") | ||
|
|
||
| # Keep the admin UI strictly on GLOBAL entries (user_id is NULL) | ||
| column_exclude_list = ("user_id", "user") | ||
| form_excluded_columns = ("user_id", "user") | ||
|
|
||
| def get_query(self): | ||
| return ( | ||
| super().get_query().filter(self.model.user_id.is_(None)) # type: ignore[attr-defined] | ||
| ) | ||
|
|
||
| def get_count_query(self): | ||
| return ( | ||
| super().get_count_query().filter(self.model.user_id.is_(None)) # type: ignore[attr-defined] | ||
| ) | ||
|
|
||
| # Help text for admins when adding patterns | ||
| form_args = { | ||
| "pattern": { | ||
| "description": r"Regex, i.e. `@domain\.com$`", | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from cachetools import TTLCache, cached | ||
|
chrisblech marked this conversation as resolved.
|
||
|
|
||
| from app.db import Session | ||
| from app.log import LOG | ||
| from app.models import GlobalSenderBlacklist | ||
| from app.regex_utils import regex_search | ||
|
|
||
|
|
||
| # Cache enabled patterns briefly to avoid a DB query per inbound email. | ||
| # Admin changes should take effect quickly but don't need to be instant. | ||
| @cached(cache=TTLCache(maxsize=128, ttl=30)) | ||
|
chrisblech marked this conversation as resolved.
Outdated
|
||
| def _get_enabled_global_patterns() -> list[str]: | ||
| return [ | ||
| r.pattern | ||
| for r in Session.query(GlobalSenderBlacklist) | ||
| .filter( | ||
| GlobalSenderBlacklist.enabled.is_(True), | ||
| GlobalSenderBlacklist.user_id.is_(None), | ||
| ) | ||
| .order_by(GlobalSenderBlacklist.id.asc()) | ||
| .all() | ||
| ] | ||
|
|
||
|
|
||
| # Per-user cache: keep it small-ish but avoid a DB query per email per user. | ||
| @cached(cache=TTLCache(maxsize=128, ttl=30)) | ||
| def _get_enabled_user_patterns(user_id: int) -> list[str]: | ||
|
chrisblech marked this conversation as resolved.
|
||
| return [ | ||
| r.pattern | ||
| for r in Session.query(GlobalSenderBlacklist) | ||
| .filter( | ||
| GlobalSenderBlacklist.enabled.is_(True), | ||
| GlobalSenderBlacklist.user_id == user_id, | ||
| ) | ||
| .order_by(GlobalSenderBlacklist.id.asc()) | ||
| .all() | ||
| ] | ||
|
|
||
|
|
||
| def is_sender_blocked_for_user(user_id: int | None, *candidates: str) -> bool: | ||
|
chrisblech marked this conversation as resolved.
Outdated
|
||
| """Return True if any candidate sender string matches: | ||
|
|
||
| - the global sender blacklist (user_id is NULL), OR | ||
| - the given user's sender blacklist (user_id matches) | ||
|
|
||
| Typical candidates: | ||
| - SMTP envelope MAIL FROM | ||
| - parsed header From address | ||
| """ | ||
|
|
||
| patterns: list[str] = [] | ||
| patterns.extend(_get_enabled_global_patterns()) | ||
| if user_id is not None: | ||
| patterns.extend(_get_enabled_user_patterns(int(user_id))) | ||
|
|
||
| if not patterns: | ||
| return False | ||
|
|
||
| for candidate in candidates: | ||
| if not candidate: | ||
| continue | ||
| # Ignore bounce/null reverse-path | ||
| if candidate == "<>": | ||
| continue | ||
|
|
||
| for pattern in patterns: | ||
| try: | ||
| if regex_search(pattern, candidate): | ||
|
chrisblech marked this conversation as resolved.
Outdated
|
||
| return True | ||
| except Exception: | ||
| # Never crash the SMTP handler because of a bad regex. | ||
| # (Global or user entry — both are user-provided.) | ||
| LOG.exception( | ||
| "Sender blacklist regex failed: user_id=%s pattern=%s candidate=%s", | ||
| user_id, | ||
| pattern, | ||
| candidate, | ||
| ) | ||
|
|
||
| return False | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.