Add mailbox search

This commit is contained in:
Adrià Casajús 2025-09-19 16:30:40 +02:00 committed by Adrià Casajús
parent 44865da68f
commit 16c54b84ac
3 changed files with 152 additions and 153 deletions

View file

@ -29,6 +29,7 @@ from app.custom_domain_validation import (
)
from app.db import Session
from app.dns_utils import get_network_dns_client
from app.errors import ProtonPartnerNotSetUp
from app.events.event_dispatcher import EventDispatcher
from app.events.generated.event_pb2 import EventContent, UserPlanChanged
from app.models import (
@ -59,7 +60,6 @@ from app.proton.proton_partner import get_proton_partner
from app.proton.proton_unlink import perform_proton_account_unlink
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.utils import sanitize_email
from app.errors import ProtonPartnerNotSetUp
def _admin_action_formatter(view, context, model, name):
@ -1310,7 +1310,53 @@ class AbuserLookupAdmin(BaseView):
result = AbuserLookupResult.from_email_or_user_id(query)
return self.render(
"admin/abuser_lookup.html",
"admin/mailbox.html",
data=result,
query=query,
)
class MailboxSearchAdmin(BaseView):
def is_accessible(self):
return current_user.is_authenticated and current_user.is_admin
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
flash("You don't have access to the admin page", "error")
return redirect(url_for("dashboard.index", next=request.url))
@expose("/", methods=["GET", "POST"])
def index(self):
query = request.args.get("query")
if query is None:
search = MailboxSearchResult()
else:
try:
mailbox_id = int(query)
mailbox = Mailbox.get_by(id=mailbox_id)
except ValueError:
mailbox = Mailbox.get_by(email=query)
search = MailboxSearchResult.from_mailbox(mailbox)
return self.render(
"admin/mailbox_search.html",
data=search,
query=query,
helper=EmailSearchHelpers,
)
class MailboxSearchResult:
def __init__(self):
self.no_match: bool = False
self.mailbox: Optional[Mailbox] = None
@staticmethod
def from_mailbox(mailbox: Optional[Mailbox]) -> CustomDomainSearchResult:
out = CustomDomainSearchResult()
if mailbox is None:
out.no_match = True
return out
out.mailbox = mailbox
return out

View file

@ -1,5 +1,4 @@
import os
import time
from datetime import timedelta
import arrow
@ -8,6 +7,7 @@ import flask_limiter
import flask_profiler
import newrelic.agent
import sentry_sdk
import time
from flask import (
Flask,
redirect,
@ -46,6 +46,7 @@ from app.admin_model import (
CustomDomainSearchAdmin,
AbuserLookupAdmin,
ForbiddenMxIpAdmin,
MailboxSearchAdmin,
)
from app.api.base import api_bp
from app.auth.base import auth_bp
@ -456,6 +457,9 @@ def init_admin(app):
admin.init_app(app, index_view=SLAdminIndexView())
admin.add_view(EmailSearchAdmin(name="Email Search", endpoint="admin.email_search"))
admin.add_view(
MailboxSearchAdmin(name="Mailbox search", endpoint="admin.mailbox_search")
)
admin.add_view(
CustomDomainSearchAdmin(
name="Custom domain search", endpoint="admin.custom_domain_search"

View file

@ -1,33 +1,8 @@
{% extends 'admin/master.html' %}
{% block head_css %}
{{ super() }}
<style>
.card-shadow {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
border-radius: 8px;
}
.domain-title {
color: white;
padding: 10px;
border-radius: 8px 8px 0 0;
}
.domain-active {
background-color: #007bff;
}
.domain-pending-deletion {
background-color: #cdca06;
}
.status-icon {
font-size: 1.2em;
}
</style>
{% endblock %}
{% macro show_user(user) -%}
<h4>
User <a href="/admin/email_search?email={{ user.email }}">{{ user.email }}</a> with ID {{ user.id }}.
</h4>
<h4>User {{ user.email }} with ID {{ user.id }}.</h4>
{% set pu = helper.partner_user(user) %}
<table class="table">
<thead>
<tr>
@ -35,15 +10,22 @@
<th scope="col">Email</th>
<th scope="col">Verified</th>
<th scope="col">Status</th>
<th scope="col">Pending deletion</th>
<th scope="col">Paid</th>
<th scope="col">Premium</th>
<th>Subscription</th>
<th>Created At</th>
<th>Updated At</th>
<th>Subdomain Quota</th>
<th>Connected with Proton account</th>
{% if user.delete_on %}<th>Actions</th>{% endif %}
</tr>
</thead>
<tbody>
<tr>
<td>{{ user.id }}</td>
<td>
<a href="/admin/email_search?email={{ user.email }}">{{ user.email }}</a>
<a href="{{ url_for("admin.email_search.index", query=user.email) }}">{{ user.email }}</a>
</td>
{% if user.activated %}
@ -57,151 +39,118 @@
{% else %}
<td class="text-success">Enabled</td>
{% endif %}
{% if user.delete_on %}
<td class="text-danger">{{ user.delete_on }}</td>
{% else %}
<td class="text-success">None</td>
{% endif %}
<td>{{ "yes" if user.is_paid() else "No" }}</td>
<td>{{ "yes" if user.is_premium() else "No" }}</td>
<td>{{ user.get_active_subscription() }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.updated_at }}</td>
<td>
<form class="d-inline"
action="{{ url_for("admin.email_search.update_subdomain_quota") }}"
method="POST">
<input type="hidden" name="user_id" value="{{ user.id }}">
<div class="input-group input-group-sm" style="max-width: 120px;">
<input type="number"
name="subdomain_quota"
value="{{ user._subdomain_quota }}"
class="form-control form-control-sm"
min="0"
max="1000"
style="width: 60px">
<button type="submit"
onclick="return confirm('Are you sure you want to update the subdomain quota for this user?');"
class="btn btn-outline-primary btn-sm">
<i class="fas fa-check" style="font-size: 0.8rem;"></i>
</button>
</div>
</form>
</td>
{% if pu %}
<td class="flex">
<a href="?query={{ pu.partner_email }}">{{ pu.partner_email }}</a>
<form class="d-inline"
action="{{ url_for("admin.email_search.delete_partner_link") }}"
method="POST">
<input type="hidden" name="user_id" value="{{ user.id }}">
<button type="submit"
onclick="return confirm('Are you sure you would like to unlink the user?');"
class="btn btn-danger d-inline">Unlink</button>
</form>
</td>
{% else %}
<td>No</td>
{% endif %}
{% if user.delete_on %}
<td>
<form class="d-inline"
action="{{ url_for("admin.email_search.stop_user_deletion") }}"
method="POST">
<input type="hidden" name="user_id" value="{{ user.id }}">
<button type="submit"
onclick="return confirm('Are you sure you want to cancel the scheduled deletion for this user?');"
class="btn btn-warning btn-sm">Stop Deletion</button>
</form>
</td>
{% endif %}
</tr>
</tbody>
</table>
{%- endmacro %}
{% macro show_verification(title, expected, errors) -%}
{% if not expected %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<h5>{{ title }}</h5>
<span class="text-success status-icon"><i class="fa fa-check-circle"></i></span>
</li>
{% else %}
<li class="list-group-item">
<h5>{{ title }}</h5>
<p>
<strong>Expected:</strong> {{ expected.recommended }}
</p>
<p>
<strong>Allowed:</strong>
<ul>
{% for expected_record in expected.allowed %}<li>{{ expected_record }}</li>{% endfor %}
</ul>
</p>
<p>
<strong>Current response:</strong>
</p>
{% for error in errors %}
<ul class="list-group">
<li class="list-group-item">{{ error }}</li>
</ul>
{% endfor %}
</li>
{% endif %}
{%- endmacro %}
{% macro show_mx_verification(title, expected, errors) -%}
{% if not expected %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<h5>{{ title }}</h5>
<span class="text-success status-icon"><i class="fa fa-check-circle"></i></span>
</li>
{% else %}
<li class="list-group-item">
<h5>{{ title }}</h5>
<ul>
<li class="list-group-item">
{% for prio in expected %}
<p>
<strong>Priority {{ prio }}:</strong> {{ expected[prio].recommended }}
</p>
<p>
<strong>Allowed:</strong>
<ul>
{% for expected_record in expected[prio].allowed %}<li>{{ expected_record }}</li>{% endfor %}
</ul>
</p>
<p>
<strong>Current response:</strong>
</p>
{% for error in errors %}
<ul class="list-group">
<li class="list-group-item">{{ error }}</li>
</ul>
{% endfor %}
</li>
{% endfor %}
</ul>
</li>
{% endif %}
{%- endmacro %}
{% macro show_domain(domain_with_data) -%}
<div class="col-md-3 mb-4">
<div class="card card-shadow">
{% if domain_with_data.domain.pending_deletion == True %}
<div class="domain-title domain-pending-deletion text-center">
<h4>Domain {{ domain_with_data.domain.domain }}</h4>
<h5>Scheduled for deletion</h5>
</div>
{% else %}
<div class="domain-title domain-active text-center">
<h4>Domain {{ domain_with_data.domain.domain }}</h4>
</div>
{% endif %}
<div class="card-body">
{% set domain = domain_with_data.domain %}
<ul class="list-group">
{{ show_verification("Ownership", domain_with_data.ownership_expected, domain_with_data.ownership_validation.errors) }}
{{ show_mx_verification("MX", domain_with_data.mx_expected, domain_with_data.mx_validation.errors) }}
{{ show_verification("SPF", domain_with_data.spf_expected, domain_with_data.spf_validation.errors) }}
{% for dkim_domain in domain_with_data.dkim_expected %}
{{ show_verification("DKIM {}.{}".format(dkim_domain, domain.domain) , domain_with_data.dkim_expected[dkim_domain], [domain_with_data.dkim_validation.get(dkim_domain+"."+domain.domain,'')]) }}
{% endfor %}
</ul>
<form class="my-3"
action="{{ url_for("admin.custom_domain_search.delete_custom_domain") }}"
method="POST">
<input type="hidden" name="domain_id" value="{{ domain.id }}">
{% if domain_with_data.domain_pending_deletion == False %}
<button type="submit"
onclick="return confirm('Are you sure you would like to delete the custom domain?');"
class="btn btn-danger w-100">Delete</button>
{% endif %}
</form>
</div>
</div>
</div>
{%- endmacro %}
{% macro show_mailbox(mailbox) %}
<table class="table">
<thead>
<tr>
<th>Mailbox ID</th>
<th>Email</th>
<th>Verified</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ mailbox.id }}</td>
<td>
<a href="?query={{ mailbox.email }}">{{ mailbox.email }}</a>
</td>
<td>{{ "Yes" if mailbox.verified else "No" }}</td>
<td>{{ mailbox.created_at }}</td>
</tr>
</tbody>
</table>
{% endmacro %}
{% block body %}
<div class="border border-dark border-2 mt-1 mb-2 p-3">
<form method="get">
<div class="form-group">
<label for="email">User or domain to search:</label>
<label for="email">Mailbox email or ID to search:</label>
<input type="text"
class="form-control"
name="user"
name="query"
value="{{ query or '' }}" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% if data.no_match and query %}
{% if not data.mailbox and query %}
<div class="border border-dark border-2 mt-1 mb-2 p-3 alert alert-warning"
role="alert">No user, alias or mailbox found for {{ query }}</div>
role="alert">No mailbox found for {{ query }}</div>
{% endif %}
{% if data.user %}
{% if data.mailbox %}
<div class="border border-dark border-2 mt-1 mb-2 p-3">
<h3 class="mb-3">Found User {{ data.user.email }}</h3>
{{ show_user(data.user) }}
<h3 class="mb-3">Found mailbox {{ data.mailbox.email }}</h3>
{{ show_mailbox(data.mailbox) }}
{{ show_user(data.mailbox.user) }}
</div>
{% endif %}
<div class="row mt-4">
{% for domain_with_data in data.domains %}{{ show_domain(domain_with_data) }}{% endfor %}
</div>
</div>
{% endblock %}