Compare commits

...

4 Commits

Author SHA1 Message Date
39fcf2e48f 4.49.3
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m11s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m17s
Build-Release-Image / Merge-Images (push) Successful in 13s
Build-Release-Image / Create-Release (push) Successful in 9s
Build-Release-Image / Notify (push) Successful in 4s
2024-08-23 12:00:07 +01:00
41a5a65f51 4.49.2
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m14s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m27s
Build-Release-Image / Merge-Images (push) Successful in 13s
Build-Release-Image / Create-Release (push) Successful in 9s
Build-Release-Image / Notify (push) Successful in 2s
2024-08-21 12:00:07 +01:00
1d0c7ec4a0 4.49.0
All checks were successful
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m32s
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m40s
Build-Release-Image / Merge-Images (push) Successful in 18s
Build-Release-Image / Create-Release (push) Successful in 10s
Build-Release-Image / Notify (push) Successful in 3s
2024-08-19 12:00:06 +01:00
4de5b8eb6d 4.48.2
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m9s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m14s
Build-Release-Image / Merge-Images (push) Successful in 24s
Build-Release-Image / Create-Release (push) Successful in 8s
Build-Release-Image / Notify (push) Successful in 3s
2024-08-09 12:00:06 +01:00
13 changed files with 478 additions and 343 deletions

View File

@ -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,
)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View 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)

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 %}

View 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 %}