mirror of
				https://github.com/simple-login/app.git
				synced 2025-11-04 13:06:38 +08:00 
			
		
		
		
	Add mailbox search
This commit is contained in:
		
							parent
							
								
									44865da68f
								
							
						
					
					
						commit
						16c54b84ac
					
				
					 3 changed files with 152 additions and 153 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue