Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
39fcf2e48f | |||
41a5a65f51 | |||
1d0c7ec4a0 | |||
4de5b8eb6d |
@ -1,3 +1,4 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
import arrow
|
||||
@ -30,6 +31,8 @@ from app.models import (
|
||||
Newsletter,
|
||||
PADDLE_SUBSCRIPTION_GRACE_DAYS,
|
||||
Mailbox,
|
||||
DeletedAlias,
|
||||
DomainDeletedAlias,
|
||||
)
|
||||
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
|
||||
|
||||
@ -729,6 +732,67 @@ class InvalidMailboxDomainAdmin(SLModelView):
|
||||
can_delete = True
|
||||
|
||||
|
||||
class EmailSearchResult:
|
||||
no_match: bool = True
|
||||
alias: Optional[Alias] = None
|
||||
mailbox: Optional[Mailbox] = None
|
||||
deleted_alias: Optional[DeletedAlias] = None
|
||||
deleted_custom_alias: Optional[DomainDeletedAlias] = None
|
||||
user: Optional[User] = None
|
||||
|
||||
@staticmethod
|
||||
def from_email(email: str) -> EmailSearchResult:
|
||||
output = EmailSearchResult()
|
||||
alias = Alias.get_by(email=email)
|
||||
if alias:
|
||||
output.alias = alias
|
||||
output.no_match = False
|
||||
return output
|
||||
user = User.get_by(email=email)
|
||||
if user:
|
||||
output.user = user
|
||||
output.no_match = False
|
||||
return output
|
||||
mailbox = Mailbox.get_by(email=email)
|
||||
if mailbox:
|
||||
output.mailbox = mailbox
|
||||
output.no_match = False
|
||||
return output
|
||||
deleted_alias = DeletedAlias.get_by(email=email)
|
||||
if deleted_alias:
|
||||
output.deleted_alias = deleted_alias
|
||||
output.no_match = False
|
||||
return output
|
||||
domain_deleted_alias = DomainDeletedAlias.get_by(email=email)
|
||||
if domain_deleted_alias:
|
||||
output.domain_deleted_alias = domain_deleted_alias
|
||||
output.no_match = False
|
||||
return output
|
||||
|
||||
|
||||
class EmailSearchHelpers:
|
||||
@staticmethod
|
||||
def mailbox_list(user: User) -> list[Mailbox]:
|
||||
return (
|
||||
Mailbox.filter_by(user_id=user.id)
|
||||
.order_by(Mailbox.id.asc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def mailbox_count(user: User) -> int:
|
||||
return Mailbox.filter_by(user_id=user.id).order_by(Mailbox.id.asc()).count()
|
||||
|
||||
@staticmethod
|
||||
def alias_list(user: User) -> list[Alias]:
|
||||
return Alias.filter_by(user_id=user.id).order_by(Alias.id.asc()).limit(10).all()
|
||||
|
||||
@staticmethod
|
||||
def alias_count(user: User) -> int:
|
||||
return Alias.filter_by(user_id=user.id).count()
|
||||
|
||||
|
||||
class EmailSearchAdmin(BaseView):
|
||||
def is_accessible(self):
|
||||
return current_user.is_authenticated and current_user.is_admin
|
||||
@ -740,25 +804,16 @@ class EmailSearchAdmin(BaseView):
|
||||
|
||||
@expose("/", methods=["GET", "POST"])
|
||||
def index(self):
|
||||
alias = None
|
||||
user = None
|
||||
mailbox = None
|
||||
no_match = False
|
||||
email = None
|
||||
search = EmailSearchResult()
|
||||
email = ""
|
||||
if request.form and request.form["email"]:
|
||||
email = request.form["email"]
|
||||
alias = Alias.get_by(email=email)
|
||||
user = User.get_by(email=email)
|
||||
mailbox = Mailbox.get_by(email=email)
|
||||
if not alias and not user and not mailbox:
|
||||
no_match = True
|
||||
email = email.strip()
|
||||
search = EmailSearchResult.from_email(email)
|
||||
|
||||
return self.render(
|
||||
"admin/alias_search.html",
|
||||
"admin/email_search.html",
|
||||
email=email,
|
||||
no_match=no_match,
|
||||
alias=alias,
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
user_aliases=lambda user_id: Alias.filter_by(user_id=user_id).all(),
|
||||
data=search,
|
||||
helper=EmailSearchHelpers,
|
||||
)
|
||||
|
@ -63,12 +63,16 @@ def get_user_if_alias_would_auto_create(
|
||||
# Prevent addresses with unicode characters (🤯) in them for now.
|
||||
validate_email(address, check_deliverability=False, allow_smtputf8=False)
|
||||
except EmailNotValidError:
|
||||
LOG.i(f"Not creating alias for {address} because email is invalid")
|
||||
return None
|
||||
|
||||
domain_and_rule = check_if_alias_can_be_auto_created_for_custom_domain(
|
||||
address, notify_user=notify_user
|
||||
)
|
||||
if DomainDeletedAlias.get_by(email=address):
|
||||
LOG.i(
|
||||
f"Not creating alias for {address} because it was previously deleted for this domain"
|
||||
)
|
||||
return None
|
||||
if domain_and_rule:
|
||||
return domain_and_rule[0].user
|
||||
@ -93,6 +97,9 @@ def check_if_alias_can_be_auto_created_for_custom_domain(
|
||||
custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
|
||||
|
||||
if not custom_domain:
|
||||
LOG.i(
|
||||
f"Cannot auto-create custom domain alias for {address} because there's no custom domain for {alias_domain}"
|
||||
)
|
||||
return None
|
||||
|
||||
user: User = custom_domain.user
|
||||
@ -108,6 +115,9 @@ def check_if_alias_can_be_auto_created_for_custom_domain(
|
||||
|
||||
if not custom_domain.catch_all:
|
||||
if len(custom_domain.auto_create_rules) == 0:
|
||||
LOG.i(
|
||||
f"Cannot create alias {address} for domain {custom_domain} because it has no catch-all and no rules"
|
||||
)
|
||||
return None
|
||||
local = get_email_local_part(address)
|
||||
|
||||
@ -121,7 +131,7 @@ def check_if_alias_can_be_auto_created_for_custom_domain(
|
||||
)
|
||||
return custom_domain, rule
|
||||
else: # no rule passes
|
||||
LOG.d("no rule passed to create %s", local)
|
||||
LOG.d(f"No rule matches auto-create {address} for domain {custom_domain}")
|
||||
return None
|
||||
LOG.d("Create alias via catchall")
|
||||
|
||||
@ -148,6 +158,7 @@ def check_if_alias_can_be_auto_created_for_a_directory(
|
||||
sep = "#"
|
||||
else:
|
||||
# if there's no directory separator in the alias, no way to auto-create it
|
||||
LOG.info(f"Cannot auto-create {address} since it has no directory separator")
|
||||
return None
|
||||
|
||||
directory_name = address[: address.find(sep)]
|
||||
@ -155,6 +166,9 @@ def check_if_alias_can_be_auto_created_for_a_directory(
|
||||
|
||||
directory = Directory.get_by(name=directory_name)
|
||||
if not directory:
|
||||
LOG.info(
|
||||
f"Cannot auto-create {address} because there is no directory for {directory_name}"
|
||||
)
|
||||
return None
|
||||
|
||||
user: User = directory.user
|
||||
@ -163,12 +177,17 @@ def check_if_alias_can_be_auto_created_for_a_directory(
|
||||
return None
|
||||
|
||||
if not user.can_create_new_alias():
|
||||
LOG.d(f"{user} can't create new directory alias {address}")
|
||||
LOG.d(
|
||||
f"{user} can't create new directory alias {address} because user cannot create aliases"
|
||||
)
|
||||
if notify_user:
|
||||
send_cannot_create_directory_alias(user, address, directory_name)
|
||||
return None
|
||||
|
||||
if directory.disabled:
|
||||
LOG.d(
|
||||
f"{user} can't create new directory alias {address} bcause directory is disabled"
|
||||
)
|
||||
if notify_user:
|
||||
send_cannot_create_directory_alias_disabled(user, address, directory_name)
|
||||
return None
|
||||
|
@ -3,7 +3,7 @@ import random
|
||||
import socket
|
||||
import string
|
||||
from ast import literal_eval
|
||||
from typing import Callable, List
|
||||
from typing import Callable, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dotenv import load_dotenv
|
||||
@ -588,3 +588,24 @@ EVENT_WEBHOOK = os.environ.get("EVENT_WEBHOOK", None)
|
||||
# We want it disabled by default, so only skip if defined
|
||||
EVENT_WEBHOOK_SKIP_VERIFY_SSL = "EVENT_WEBHOOK_SKIP_VERIFY_SSL" in os.environ
|
||||
EVENT_WEBHOOK_DISABLE = "EVENT_WEBHOOK_DISABLE" in os.environ
|
||||
|
||||
|
||||
def read_webhook_enabled_user_ids() -> Optional[List[int]]:
|
||||
user_ids = os.environ.get("EVENT_WEBHOOK_ENABLED_USER_IDS", None)
|
||||
if user_ids is None:
|
||||
return None
|
||||
|
||||
ids = []
|
||||
for user_id in user_ids.split(","):
|
||||
try:
|
||||
ids.append(int(user_id.strip()))
|
||||
except ValueError:
|
||||
pass
|
||||
return ids
|
||||
|
||||
|
||||
EVENT_WEBHOOK_ENABLED_USER_IDS: Optional[List[int]] = read_webhook_enabled_user_ids()
|
||||
|
||||
# Allow to define a different DB_URI for the event listener, in case we want to skip the connection pool
|
||||
# It defaults to the regular DB_URI in case it's needed
|
||||
EVENT_LISTENER_DB_URI = os.environ.get("EVENT_LISTENER_DB_URI", DB_URI)
|
||||
|
@ -548,7 +548,9 @@ def can_create_directory_for_address(email_address: str) -> bool:
|
||||
for domain in config.ALIAS_DOMAINS:
|
||||
if email_address.endswith("@" + domain):
|
||||
return True
|
||||
|
||||
LOG.i(
|
||||
f"Cannot create address in directory for {email_address} since it does not belong to a valid directory domain"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
|
@ -40,6 +40,10 @@ class EventDispatcher:
|
||||
if not config.EVENT_WEBHOOK and skip_if_webhook_missing:
|
||||
return
|
||||
|
||||
if config.EVENT_WEBHOOK_ENABLED_USER_IDS is not None:
|
||||
if user.id not in config.EVENT_WEBHOOK_ENABLED_USER_IDS:
|
||||
return
|
||||
|
||||
partner_user = EventDispatcher.__partner_user(user.id)
|
||||
if not partner_user:
|
||||
return
|
||||
|
@ -2,8 +2,9 @@ import argparse
|
||||
from enum import Enum
|
||||
from sys import argv, exit
|
||||
|
||||
from app.config import DB_URI
|
||||
from app.config import EVENT_LISTENER_DB_URI
|
||||
from app.log import LOG
|
||||
from events import event_debugger
|
||||
from events.runner import Runner
|
||||
from events.event_source import DeadLetterEventSource, PostgresEventSource
|
||||
from events.event_sink import ConsoleEventSink, HttpEventSink
|
||||
@ -31,7 +32,7 @@ def main(mode: Mode, dry_run: bool, max_retries: int):
|
||||
source = DeadLetterEventSource(max_retries)
|
||||
elif mode == Mode.LISTENER:
|
||||
LOG.i("Using PostgresEventSource")
|
||||
source = PostgresEventSource(DB_URI)
|
||||
source = PostgresEventSource(EVENT_LISTENER_DB_URI)
|
||||
else:
|
||||
raise ValueError(f"Invalid mode: {mode}")
|
||||
|
||||
@ -46,32 +47,67 @@ def main(mode: Mode, dry_run: bool, max_retries: int):
|
||||
runner.run()
|
||||
|
||||
|
||||
def debug_event(event_id: str):
|
||||
LOG.i(f"Debugging event {event_id}")
|
||||
try:
|
||||
event_id_int = int(event_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid event id: {event_id}")
|
||||
event_debugger.debug_event(event_id_int)
|
||||
|
||||
|
||||
def run_event(event_id: str, delete_on_success: bool):
|
||||
LOG.i(f"Running event {event_id}")
|
||||
try:
|
||||
event_id_int = int(event_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid event id: {event_id}")
|
||||
event_debugger.run_event(event_id_int, delete_on_success)
|
||||
|
||||
|
||||
def args():
|
||||
parser = argparse.ArgumentParser(description="Run event listener")
|
||||
parser.add_argument(
|
||||
"mode",
|
||||
help="Mode to run",
|
||||
choices=[Mode.DEAD_LETTER.value, Mode.LISTENER.value],
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
listener_parser = subparsers.add_parser(Mode.LISTENER.value)
|
||||
listener_parser.add_argument(
|
||||
"--max-retries", type=int, default=_DEFAULT_MAX_RETRIES
|
||||
)
|
||||
parser.add_argument(
|
||||
"max_retries",
|
||||
help="Max retries to consider an event as error and not try to process it again",
|
||||
type=int,
|
||||
nargs="?",
|
||||
default=_DEFAULT_MAX_RETRIES,
|
||||
listener_parser.add_argument("--dry-run", action="store_true")
|
||||
|
||||
dead_letter_parser = subparsers.add_parser(Mode.DEAD_LETTER.value)
|
||||
dead_letter_parser.add_argument(
|
||||
"--max-retries", type=int, default=_DEFAULT_MAX_RETRIES
|
||||
)
|
||||
parser.add_argument("--dry-run", help="Dry run mode", action="store_true")
|
||||
dead_letter_parser.add_argument("--dry-run", action="store_true")
|
||||
|
||||
debug_parser = subparsers.add_parser("debug")
|
||||
debug_parser.add_argument("event_id", help="ID of the event to debug")
|
||||
|
||||
run_parser = subparsers.add_parser("run")
|
||||
run_parser.add_argument("event_id", help="ID of the event to run")
|
||||
run_parser.add_argument("--delete-on-success", action="store_true")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(argv) < 2:
|
||||
print("Invalid usage. Pass 'listener' or 'dead_letter' as argument")
|
||||
print("Invalid usage. Pass a valid subcommand as argument")
|
||||
exit(1)
|
||||
|
||||
args = args()
|
||||
main(
|
||||
mode=Mode.from_str(args.mode),
|
||||
dry_run=args.dry_run,
|
||||
max_retries=args.max_retries,
|
||||
)
|
||||
|
||||
if args.command in [Mode.LISTENER.value, Mode.DEAD_LETTER.value]:
|
||||
main(
|
||||
mode=Mode.from_str(args.command),
|
||||
dry_run=args.dry_run,
|
||||
max_retries=args.max_retries,
|
||||
)
|
||||
elif args.command == "debug":
|
||||
debug_event(args.event_id)
|
||||
elif args.command == "run":
|
||||
run_event(args.event_id, args.delete_on_success)
|
||||
else:
|
||||
print("Invalid command")
|
||||
exit(1)
|
||||
|
43
app/events/event_debugger.py
Normal file
43
app/events/event_debugger.py
Normal file
@ -0,0 +1,43 @@
|
||||
from app.events.generated import event_pb2
|
||||
from app.models import SyncEvent
|
||||
from events.event_sink import HttpEventSink
|
||||
|
||||
|
||||
def debug_event(event_id: int):
|
||||
event = SyncEvent.get_by(id=event_id)
|
||||
if not event:
|
||||
print("Event not found")
|
||||
return
|
||||
|
||||
print(f"Info for event {event_id}")
|
||||
print(f"- Created at: {event.created_at}")
|
||||
print(f"- Updated at: {event.updated_at}")
|
||||
print(f"- Taken time: {event.taken_time}")
|
||||
print(f"- Retry count: {event.retry_count}")
|
||||
|
||||
print()
|
||||
print("Event contents")
|
||||
event_contents = event.content
|
||||
parsed = event_pb2.Event.FromString(event_contents)
|
||||
|
||||
print(f"- UserID: {parsed.user_id}")
|
||||
print(f"- ExternalUserID: {parsed.external_user_id}")
|
||||
print(f"- PartnerID: {parsed.partner_id}")
|
||||
|
||||
content = parsed.content
|
||||
print(f"Content: {content}")
|
||||
|
||||
|
||||
def run_event(event_id: int, delete_on_success: bool = True):
|
||||
event = SyncEvent.get_by(id=event_id)
|
||||
if not event:
|
||||
print("Event not found")
|
||||
return
|
||||
|
||||
print(f"Processing event {event_id}")
|
||||
sink = HttpEventSink()
|
||||
res = sink.process(event)
|
||||
if res:
|
||||
print(f"Processed event {event_id}")
|
||||
if delete_on_success:
|
||||
SyncEvent.delete(event_id, commit=True)
|
@ -46,6 +46,7 @@ class PostgresEventSource(EventSource):
|
||||
cursor = self.__connection.cursor()
|
||||
cursor.execute(f"LISTEN {NOTIFICATION_CHANNEL};")
|
||||
|
||||
LOG.info("Starting to listen to events")
|
||||
while True:
|
||||
if select.select([self.__connection], [], [], 5) != ([], [], []):
|
||||
self.__connection.poll()
|
||||
|
@ -14,6 +14,7 @@ from app.email_utils import (
|
||||
send_email,
|
||||
render,
|
||||
)
|
||||
from app.events.event_dispatcher import PostgresDispatcher
|
||||
from app.import_utils import handle_batch_import
|
||||
from app.jobs.event_jobs import send_alias_creation_events_for_user
|
||||
from app.jobs.export_user_data_job import ExportUserDataJob
|
||||
@ -276,7 +277,9 @@ SimpleLogin team.
|
||||
user = User.get(user_id)
|
||||
if user and user.activated:
|
||||
LOG.d(f"Sending alias creation events for {user}")
|
||||
send_alias_creation_events_for_user(user)
|
||||
send_alias_creation_events_for_user(
|
||||
user, dispatcher=PostgresDispatcher.get()
|
||||
)
|
||||
else:
|
||||
LOG.e("Unknown job name %s", job.name)
|
||||
|
||||
|
@ -3,5 +3,5 @@
|
||||
export DB_URI=postgresql://myuser:mypassword@localhost:15432/simplelogin
|
||||
echo 'drop schema public cascade; create schema public;' | psql $DB_URI
|
||||
|
||||
rye run alembic upgrade head
|
||||
rye run flask dummy-data
|
||||
poetry run alembic upgrade head
|
||||
poetry run flask dummy-data
|
||||
|
@ -3,4 +3,4 @@
|
||||
export DB_URI=postgresql://myuser:mypassword@localhost:15432/test
|
||||
echo 'drop schema public cascade; create schema public;' | psql $DB_URI
|
||||
|
||||
rye run alembic upgrade head
|
||||
poetry run alembic upgrade head
|
||||
|
@ -1,300 +0,0 @@
|
||||
{% extends 'admin/master.html' %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="email">Email to search:</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="email"
|
||||
value="{{ email or '' }}"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if no_match %}
|
||||
|
||||
<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 {{ email }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if alias %}
|
||||
|
||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||
<h3>Alias {{ alias.email }} found</h3>
|
||||
<dl>
|
||||
<dt>
|
||||
Alias id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.email }}
|
||||
</dd>
|
||||
<dt>
|
||||
Created at
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.created_at }}
|
||||
</dd>
|
||||
<dt class="mb-2">
|
||||
User
|
||||
</dt>
|
||||
<dd class="ml-4 border-secondary border p-2">
|
||||
<dl>
|
||||
<dt>
|
||||
User id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.user.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.user.email }}
|
||||
</dd>
|
||||
<dt>
|
||||
Premium
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.user.is_premium() }}
|
||||
</dd>
|
||||
<dt>
|
||||
Disabled
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.user.disabled }}
|
||||
</dd>
|
||||
<dt>
|
||||
Crated At
|
||||
</dt>
|
||||
<dd>
|
||||
{{ alias.user.created_at }}
|
||||
</dd>
|
||||
<dt class="border-dark border-top-2 mb-2">
|
||||
Mailboxes
|
||||
</dt>
|
||||
<dd>
|
||||
{% for mailbox in alias.mailboxes %}
|
||||
|
||||
<dl class="border border-grey border-2 ml-4 p-2">
|
||||
<dt>
|
||||
Mailbox id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.email }}
|
||||
</dd>
|
||||
<dt>
|
||||
Verified
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.verified }}
|
||||
</dd>
|
||||
<dt>
|
||||
Created At
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.created_at }}
|
||||
</dd>
|
||||
</dl>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user %}
|
||||
|
||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||
<h3>User {{ user.email }} found</h3>
|
||||
<dl>
|
||||
<dt>
|
||||
User id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ user.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ user.email }}
|
||||
</dd>
|
||||
{% if user.is_paid() %}
|
||||
|
||||
<dt>
|
||||
Paid
|
||||
</dt>
|
||||
<dd>
|
||||
Yes
|
||||
</dd>
|
||||
<dt>
|
||||
Subscription
|
||||
</dt>
|
||||
<dd>
|
||||
{{ user.get_active_subscription() }}
|
||||
</dd>
|
||||
{% else %}
|
||||
<dt>
|
||||
Paid
|
||||
</dt>
|
||||
<dd>
|
||||
No
|
||||
</dd>
|
||||
{% endif %}
|
||||
<dt>
|
||||
Created at
|
||||
</dt>
|
||||
<dd>
|
||||
{{ user.created_at }}
|
||||
</dd>
|
||||
<dt class="mb-2">
|
||||
Mailboxes
|
||||
</dt>
|
||||
<dd>
|
||||
{% for mailbox in user.mailboxes() %}
|
||||
|
||||
<dl class="border border-dark p-2 ml-4">
|
||||
<dt>
|
||||
Mailbox id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.email }}
|
||||
</dd>
|
||||
<dt>
|
||||
Verified
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.verified }}
|
||||
</dd>
|
||||
<dt>
|
||||
Created At
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.created_at }}
|
||||
</dd>
|
||||
</dl>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
<dt class="mb-2">
|
||||
Aliases
|
||||
</dt>
|
||||
<dd>
|
||||
{% for mailbox in user_aliases(user.id) %}
|
||||
|
||||
<dl class="border border-dark p-2 ml-4">
|
||||
<dt>
|
||||
Mailbox id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.email }}
|
||||
</dd>
|
||||
<dt>
|
||||
Verified
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.verified }}
|
||||
</dd>
|
||||
<dt>
|
||||
Created At
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.created_at }}
|
||||
</dd>
|
||||
</dl>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mailbox %}
|
||||
|
||||
<div class="border border-dark mt-1 mb-2 p-3">
|
||||
<h3>Mailbox {{ mailbox.email }} found</h3>
|
||||
<dl>
|
||||
<dt>
|
||||
Mailbox id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.email }}
|
||||
</dd>
|
||||
<dt>
|
||||
Created at
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.created_at }}
|
||||
</dd>
|
||||
<dt class="mb-2">
|
||||
User
|
||||
</dt>
|
||||
<dd class="ml-4">
|
||||
<dl class="border-dark border p-2">
|
||||
<dt>
|
||||
User id
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.user.id }}
|
||||
</dd>
|
||||
<dt>
|
||||
Email
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.user.email }}
|
||||
</dd>
|
||||
<dt>
|
||||
Premium
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.user.is_premium() }}
|
||||
</dd>
|
||||
<dt>
|
||||
Disabled
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.user.disabled }}
|
||||
</dd>
|
||||
<dt>
|
||||
Crated At
|
||||
</dt>
|
||||
<dd>
|
||||
{{ mailbox.user.created_at }}
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
251
app/templates/admin/email_search.html
Normal file
251
app/templates/admin/email_search.html
Normal file
@ -0,0 +1,251 @@
|
||||
{% extends 'admin/master.html' %}
|
||||
|
||||
{% macro show_user(user) -%}
|
||||
<h4>User {{ user.email }} with ID {{ user.id }}.</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">User ID</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Paid</th>
|
||||
<th>Subscription</th>
|
||||
<th>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ "yes" if user.is_paid() else No }}</td>
|
||||
<td>{{ user.get_active_subscription() }}</td>
|
||||
<td>{{ user.created_at }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
{% macro list_mailboxes(mbox_count, mboxes) %}
|
||||
<h4>
|
||||
{{ mbox_count }} Mailboxes found.
|
||||
{% if mbox_count>10 %}Showing only the first 10.{% endif %}
|
||||
</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mailbox ID</th>
|
||||
<th>Email</th>
|
||||
<th>Verified</th>
|
||||
<th>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mailbox in mboxes %}
|
||||
|
||||
<tr>
|
||||
<td>{{ mailbox.id }}</td>
|
||||
<td>{{ mailbox.email }}</td>
|
||||
<td>{{ "Yes" if mailbox.verified else "No" }}</td>
|
||||
<td>{{ mailbox.created_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
{% macro list_alias(alias_count, aliases) %}
|
||||
<h4>
|
||||
{{ alias_count }} Aliases found.
|
||||
{% if alias_count>10 %}Showing only the first 10.{% endif %}
|
||||
</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias ID</th>
|
||||
<th>Email</th>
|
||||
<th>Verified</th>
|
||||
<th>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for alias in aliases %}
|
||||
|
||||
<tr>
|
||||
<td>{{ alias.id }}</td>
|
||||
<td>{{ alias.email }}</td>
|
||||
<td>{{ "Yes" if alias.verified else "No" }}</td>
|
||||
<td>
|
||||
{{ alias.created_at }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
{% macro show_deleted_alias(deleted_alias) -%}
|
||||
<h4>
|
||||
Deleted Alias {{ deleted_alias.email }} with ID {{ deleted_alias.id }}.
|
||||
</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
Deleted Alias ID
|
||||
</th>
|
||||
<th scope="col">
|
||||
Email
|
||||
</th>
|
||||
<th scope="col">
|
||||
Deleted At
|
||||
</th>
|
||||
<th scope="col">
|
||||
Reason
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ deleted_alias.id }}
|
||||
</td>
|
||||
<td>
|
||||
{{ deleted_alias.email }}
|
||||
</td>
|
||||
<td>
|
||||
{{ deleted_alias.created_at }}
|
||||
</td>
|
||||
<td>
|
||||
{{ deleted_alias.reason }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
{% macro show_domain_deleted_alias(dom_deleted_alias) -%}
|
||||
<h4>
|
||||
Domain Deleted Alias {{ dom_deleted_alias.email }} with ID {{ dom_deleted_alias.id }} for domain {{ dom_deleted_alias.domain.domain }}
|
||||
</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
Deleted Alias ID
|
||||
</th>
|
||||
<th scope="col">
|
||||
Email
|
||||
</th>
|
||||
<th scope="col">
|
||||
Domain
|
||||
</th>
|
||||
<th scope="col">
|
||||
Domain ID
|
||||
</th>
|
||||
<th scope="col">
|
||||
Domain owner user ID
|
||||
</th>
|
||||
<th scope="col">
|
||||
Domain owner user email
|
||||
</th>
|
||||
<th scope="col">
|
||||
Deleted At
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{ dom_deleted_alias.id }}
|
||||
</td>
|
||||
<td>
|
||||
{{ dom_deleted_alias.email }}
|
||||
</td>
|
||||
<td>
|
||||
{{ dom_deleted_alias.domain.domain }}
|
||||
</td>
|
||||
<td>
|
||||
{{ dom_deleted_alias.domain.id }}
|
||||
</td>
|
||||
<td>
|
||||
{{ dom_deleted_alias.domain.user_id }}
|
||||
</td>
|
||||
<td>
|
||||
{{ dom_deleted_alias.created_at }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ show_user(data.domain_deleted_alias.domain.user) }}
|
||||
{%- endmacro %}
|
||||
{% block body %}
|
||||
|
||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="email">
|
||||
Email to search:
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="email"
|
||||
value="{{ email or '' }}"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if no_match %}
|
||||
|
||||
<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 {{ email }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.alias %}
|
||||
|
||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||
<h3 class="mb-3">
|
||||
Found Alias {{ data.alias.email }}
|
||||
</h3>
|
||||
{{ list_alias(1,[data.alias]) }}
|
||||
{{ show_user(data.alias.user) }}
|
||||
{{ list_mailboxes(helper.mailbox_count(data.alias.user), helper.mailbox_list(data.alias.user) ) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.user %}
|
||||
|
||||
<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) }}
|
||||
{{ list_mailboxes(helper.mailbox_count(data.user), helper.mailbox_list(data.user) ) }}
|
||||
{{ list_alias(helper.alias_count(data.user),helper.alias_list(data.user)) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.mailbox %}
|
||||
|
||||
<div class="border border-dark mt-1 mb-2 p-3">
|
||||
<h3 class="mb-3">
|
||||
Found Mailbox {{ data.mailbox.email }}
|
||||
</h3>
|
||||
{{ list_mailboxes(1, [data.mailbox] ) }}
|
||||
{{ show_user(data.mailbox.user) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.deleted_alias %}
|
||||
|
||||
<div class="border border-dark mt-1 mb-2 p-3">
|
||||
<h3 class="mb-3">
|
||||
Found DeletedAlias {{ data.deleted_alias.email }}
|
||||
</h3>
|
||||
{{ show_deleted_alias(data.deleted_alias) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if data.domain_deleted_alias %}
|
||||
|
||||
<div class="border border-dark mt-1 mb-2 p-3">
|
||||
<h3 class="mb-3">
|
||||
Found DomainDeletedAlias {{ data.domain_deleted_alias.email }}
|
||||
</h3>
|
||||
{{ show_domain_deleted_alias(data.domain_deleted_alias) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
Reference in New Issue
Block a user