Compare commits

...

6 Commits

Author SHA1 Message Date
8896f00124 4.49.5
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m16s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m21s
Build-Release-Image / Merge-Images (push) Successful in 11s
Build-Release-Image / Create-Release (push) Successful in 9s
Build-Release-Image / Notify (push) Successful in 2s
2024-08-26 12:00:07 +01:00
d313c94f77 4.49.4
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m6s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m35s
Build-Release-Image / Merge-Images (push) Successful in 14s
Build-Release-Image / Create-Release (push) Successful in 10s
Build-Release-Image / Notify (push) Successful in 3s
2024-08-24 12:00:07 +01:00
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
23 changed files with 497 additions and 361 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
@ -311,7 +330,10 @@ def try_auto_create_via_domain(address: str) -> Optional[Alias]:
def delete_alias(
alias: Alias, user: User, reason: AliasDeleteReason = AliasDeleteReason.Unspecified
alias: Alias,
user: User,
reason: AliasDeleteReason = AliasDeleteReason.Unspecified,
commit: bool = False,
):
"""
Delete an alias and add it to either global or domain trash
@ -347,6 +369,8 @@ def delete_alias(
EventDispatcher.send_event(
user, EventContent(alias_deleted=AliasDeleted(alias_id=alias.id))
)
if commit:
Session.commit()
def aliases_for_mailbox(mailbox: Mailbox) -> [Alias]:

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

@ -145,7 +145,7 @@ def index():
LOG.i(f"User {current_user} requested deletion of alias {alias}")
email = alias.email
alias_utils.delete_alias(
alias, current_user, AliasDeleteReason.ManualAction
alias, current_user, AliasDeleteReason.ManualAction, commit=True
)
flash(f"Alias {email} has been deleted", "success")
elif request.form.get("form-name") == "disable-alias":

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,6 +2,7 @@ import requests
from requests import RequestException
from app import config
from app.db import Session
from app.events.event_dispatcher import EventDispatcher
from app.events.generated.event_pb2 import EventContent, UserPlanChanged
from app.log import LOG
@ -29,10 +30,11 @@ def execute_subscription_webhook(user: User):
LOG.i("Sent request to subscription update webhook successfully")
else:
LOG.i(
f"Request to webhook failed with statue {response.status_code}: {response.text}"
f"Request to webhook failed with status {response.status_code}: {response.text}"
)
except RequestException as e:
LOG.error(f"Subscription request exception: {e}")
event = UserPlanChanged(plan_end_time=sl_subscription_end)
EventDispatcher.send_event(user, EventContent(user_plan_change=event))
Session.commit()

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

View File

@ -18,7 +18,7 @@
<br />
For generic questions, i.e. not related to your account, we recommend to post the question on
our
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a> or <a href="https://forum.simplelogin.io/">our official forum</a>
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a> or <a href="https://github.com/simple-login/app/discussions">forum</a>
where our community can help answer the question
and other people with the same question can find the answer there.
</div>

View File

@ -43,8 +43,7 @@ Note, if you are a paying Proton Mail user, you automatically receive the premiu
{% endcall %}
{% call text() %}
For any question or feedback, please join our <a href="https://forum.simplelogin.io/">official forum</a>.
If you want to request a feature, please submit it on our <a href="https://github.com/simple-login/app/discussions">GitHub repo</a>.
For any question or if you want to request a feature, please submit it on our <a href="https://github.com/simple-login/app/discussions">forum</a>.
You can also join our
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
or follow our

View File

@ -27,8 +27,7 @@ Firefox: https://addons.mozilla.org/firefox/addon/simplelogin/
Edge: https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff
Android: https://play.google.com/store/apps/details?id=io.simplelogin.android
iOS: https://apps.apple.com/app/id1494359858
Github repo: https://github.com/simple-login/app/discussions
Official forum: https://forum.simplelogin.io/
Forum: https://github.com/simple-login/app/discussions
Reddit: https://www.reddit.com/r/Simplelogin/
Twitter: https://twitter.com/simple_login

View File

@ -71,10 +71,8 @@ Please note that you can't create more than {{ MAX_NB_EMAIL_FREE_PLAN }} aliases
{% endif %}
{% call text() %}
For any question or feedback,
please join our <a href="https://forum.simplelogin.io/">official forum</a>.
If you want to request a feature,
please submit it on our <a href="https://github.com/simple-login/app/discussions">GitHub repo</a>.
For any question or if you want to request a feature,
please submit it on our <a href="https://github.com/simple-login/app/discussions">forum</a>.
You can also join our
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
or follow our

View File

@ -26,8 +26,6 @@ No worries: all aliases you create during this period will continue to work norm
At any time, you can reach out to us by simply replying to this email.
For any question or feedback, please join our official forum at https://forum.simplelogin.io/
If you want to request a feature, please submit it on our GitHub repo at https://github.com/simple-login/app/discussions
For any question or if you want to request a feature, please submit it on our forum at https://github.com/simple-login/app/discussions
You can also join our Reddit at https://www.reddit.com/r/Simplelogin/ follow our Twitter at https://twitter.com/simplelogin

View File

@ -93,7 +93,7 @@
</li>
<li>
<a class="list-group-item text-white footer-item "
href="https://forum.simplelogin.io">Forum</a>
href="https://github.com/simple-login/app/discussions">Forum</a>
</li>
</ul>
</div>

View File

@ -91,7 +91,7 @@
</a>
</div>
<div class="dropdown-item">
<a href="https://forum.simplelogin.io"
<a href="https://github.com/simple-login/app/discussions"
target="_blank"
rel="noopener noreferrer">
Forum

View File

@ -107,7 +107,7 @@
</a>
</div>
<div class="dropdown-item">
<a href="https://forum.simplelogin.io/"
<a href="https://github.com/simple-login/app/discussions"
target="_blank"
rel="noopener noreferrer">
Forum