Compare commits

...

7 Commits

Author SHA1 Message Date
3ecc8d36f9 4.49.8
Some checks failed
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m18s
Build-Release-Image / Build-Image (linux/amd64) (push) Has been cancelled
Build-Release-Image / Merge-Images (push) Has been cancelled
Build-Release-Image / Create-Release (push) Has been cancelled
Build-Release-Image / Notify (push) Has been cancelled
2024-09-04 12:00:07 +01:00
14f4829fab 4.49.7
Some checks failed
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m21s
Build-Release-Image / Build-Image (linux/amd64) (push) Has been cancelled
Build-Release-Image / Merge-Images (push) Has been cancelled
Build-Release-Image / Create-Release (push) Has been cancelled
Build-Release-Image / Notify (push) Has been cancelled
2024-09-03 12:00:06 +01:00
63ac89e952 4.49.6
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m15s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m18s
Build-Release-Image / Merge-Images (push) Successful in 13s
Build-Release-Image / Create-Release (push) Successful in 10s
Build-Release-Image / Notify (push) Successful in 3s
2024-08-28 12:00:07 +01:00
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
125 changed files with 2197 additions and 2372 deletions

View File

@ -8,7 +8,7 @@ repos:
- id: check-yaml
- id: trailing-whitespace
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.3.0
rev: v1.34.1
hooks:
- id: djlint-jinja
files: '.*\.html'
@ -22,4 +22,3 @@ repos:
args: [ --fix ]
# Run the formatter.
- id: ruff-format

View File

@ -20,7 +20,7 @@ SimpleLogin backend consists of 2 main components:
## Install dependencies
The project requires:
- Python 3.10 and [rye](https://github.com/astral-sh/rye) to manage dependencies
- Python 3.10 and poetry to manage dependencies
- Node v10 for front-end.
- Postgres 13+
@ -28,7 +28,7 @@ First, install all dependencies by running the following command.
Feel free to use `virtualenv` or similar tools to isolate development environment.
```bash
rye sync
poetry sync
```
On Mac, sometimes you might need to install some other packages via `brew`:
@ -55,7 +55,7 @@ brew install -s re2 pybind11
We use pre-commit to run all our linting and static analysis checks. Please run
```bash
rye run pre-commit install
poetry run pre-commit install
```
To install it in your development environment.
@ -160,25 +160,25 @@ Here are the small sum-ups of the directory structures and their roles:
The code is formatted using [ruff](https://github.com/astral-sh/ruff), to format the code, simply run
```
rye run ruff format .
poetry run ruff format .
```
The code is also checked with `flake8`, make sure to run `flake8` before creating the pull request by
```bash
rye run flake8
poetry run flake8
```
For HTML templates, we use `djlint`. Before creating a pull request, please run
```bash
rye run djlint --check templates
poetry run djlint --check templates
```
If some files aren't properly formatted, you can format all files with
```bash
rye run djlint --reformat .
poetry run djlint --reformat .
```
## Test sending email
@ -223,6 +223,31 @@ Now open http://localhost:1080/ (or http://localhost:1080/ for MailHog), you sho
## Job runner
Some features require a job handler (such as GDPR data export). To test such feature you need to run the job_runner
```bash
python job_runner.py
```
# Setup for Mac
There are several ways to setup Python and manage the project dependencies on Mac. For info we have successfully used this setup on a Mac silicon:
```bash
# we haven't managed to make python 3.12 work
brew install python3.10
# make sure to update the PATH so python, pip point to Python3
# for us it can be done by adding "export PATH=/opt/homebrew/opt/python@3.10/libexec/bin:$PATH" to .zprofile
# Although pipx is the recommended way to install poetry,
# install pipx via brew will automatically install python 3.12
# and poetry will then use python 3.12
# so we recommend using poetry this way instead
curl -sSL https://install.python-poetry.org | python3 -
poetry install
# activate the virtualenv and you should be good to go!
source .venv/bin/activate
```

View File

@ -33,6 +33,7 @@ from app.models import (
Mailbox,
DeletedAlias,
DomainDeletedAlias,
PartnerUser,
)
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
@ -735,7 +736,8 @@ class InvalidMailboxDomainAdmin(SLModelView):
class EmailSearchResult:
no_match: bool = True
alias: Optional[Alias] = None
mailbox: Optional[Mailbox] = None
mailbox: list[Mailbox] = []
mailbox_count: int = 0
deleted_alias: Optional[DeletedAlias] = None
deleted_custom_alias: Optional[DomainDeletedAlias] = None
user: Optional[User] = None
@ -747,22 +749,21 @@ class EmailSearchResult:
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
mailboxes = (
Mailbox.filter_by(email=email).order_by(Mailbox.id.desc()).limit(10).all()
)
if mailboxes:
output.mailbox = mailboxes
output.mailbox_count = Mailbox.filter_by(email=email).count()
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
@ -782,16 +783,22 @@ class EmailSearchHelpers:
@staticmethod
def mailbox_count(user: User) -> int:
return Mailbox.filter_by(user_id=user.id).order_by(Mailbox.id.asc()).count()
return Mailbox.filter_by(user_id=user.id).order_by(Mailbox.id.desc()).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()
return (
Alias.filter_by(user_id=user.id).order_by(Alias.id.desc()).limit(10).all()
)
@staticmethod
def alias_count(user: User) -> int:
return Alias.filter_by(user_id=user.id).count()
@staticmethod
def partner_user(user: User) -> Optional[PartnerUser]:
return PartnerUser.get_by(user_id=user.id)
class EmailSearchAdmin(BaseView):
def is_accessible(self):
@ -805,9 +812,8 @@ class EmailSearchAdmin(BaseView):
@expose("/", methods=["GET", "POST"])
def index(self):
search = EmailSearchResult()
email = ""
if request.form and request.form["email"]:
email = request.form["email"]
email = request.args.get("email")
if email is not None and len(email) > 0:
email = email.strip()
search = EmailSearchResult.from_email(email)

View File

@ -330,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
@ -366,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

@ -596,12 +596,16 @@ def read_webhook_enabled_user_ids() -> Optional[List[int]]:
return None
ids = []
for id in user_ids.split(","):
for user_id in user_ids.split(","):
try:
ids.append(int(id.strip()))
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

@ -1,8 +1,12 @@
from abc import ABC, abstractmethod
import newrelic.agent
from app import config
from app.db import Session
from app.errors import ProtonPartnerNotSetUp
from app.events.generated import event_pb2
from app.log import LOG
from app.models import User, PartnerUser, SyncEvent
from app.proton.utils import get_proton_partner
from typing import Optional
@ -35,9 +39,13 @@ class EventDispatcher:
skip_if_webhook_missing: bool = True,
):
if config.EVENT_WEBHOOK_DISABLE:
LOG.i("Not sending events because webhook is disabled")
return
if not config.EVENT_WEBHOOK and skip_if_webhook_missing:
LOG.i(
"Not sending events because webhook is not configured and allowed to be empty"
)
return
if config.EVENT_WEBHOOK_ENABLED_USER_IDS is not None:
@ -46,6 +54,9 @@ class EventDispatcher:
partner_user = EventDispatcher.__partner_user(user.id)
if not partner_user:
LOG.i(
f"Not sending events because there's no partner user for user {user}"
)
return
event = event_pb2.Event(
@ -57,6 +68,8 @@ class EventDispatcher:
serialized = event.SerializeToString()
dispatcher.send(serialized)
newrelic.agent.record_custom_metric("Custom/events_stored", 1)
LOG.i("Sent event to the dispatcher")
@staticmethod
def __partner_user(user_id: int) -> Optional[PartnerUser]:

View File

@ -1,3 +1,5 @@
import newrelic.agent
from app.events.event_dispatcher import EventDispatcher, Dispatcher
from app.events.generated.event_pb2 import EventContent, AliasCreated, AliasCreatedList
from app.log import LOG
@ -12,6 +14,7 @@ def send_alias_creation_events_for_user(
return
chunk_size = min(chunk_size, 50)
event_list = []
LOG.i("Sending alias create events for user {user}")
for alias in (
Alias.yield_per_query(chunk_size)
.filter_by(user_id=user.id)
@ -26,15 +29,23 @@ def send_alias_creation_events_for_user(
)
)
if len(event_list) >= chunk_size:
LOG.i(f"Sending {len(event_list)} alias create event for {user}")
EventDispatcher.send_event(
user,
EventContent(alias_create_list=AliasCreatedList(events=event_list)),
dispatcher=dispatcher,
)
newrelic.agent.record_custom_metric(
"Custom/event_alias_created_event", len(event_list)
)
event_list = []
if len(event_list) > 0:
LOG.i(f"Sending {len(event_list)} alias create event for {user}")
EventDispatcher.send_event(
user,
EventContent(alias_create_list=AliasCreatedList(events=event_list)),
dispatcher=dispatcher,
)
newrelic.agent.record_custom_metric(
"Custom/event_alias_created_event", len(event_list)
)

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

@ -16,12 +16,13 @@ class CannotSetMailbox(Exception):
def set_default_alias_domain(user: User, domain_name: Optional[str]):
if domain_name is None:
if not domain_name:
LOG.i(f"User {user} has set no domain as default domain")
user.default_alias_public_domain_id = None
user.default_alias_custom_domain_id = None
Session.flush()
return
sl_domain: SLDomain = SLDomain.get_by(domain=domain_name)
if sl_domain:
if sl_domain.hidden:

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()
if args.command in [Mode.LISTENER.value, Mode.DEAD_LETTER.value]:
main(
mode=Mode.from_str(args.mode),
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

@ -1,4 +1,5 @@
import requests
import newrelic.agent
from abc import ABC, abstractmethod
from app.config import EVENT_WEBHOOK, EVENT_WEBHOOK_SKIP_VERIFY_SSL
@ -26,6 +27,7 @@ class HttpEventSink(EventSink):
headers={"Content-Type": "application/x-protobuf"},
verify=not EVENT_WEBHOOK_SKIP_VERIFY_SSL,
)
newrelic.agent.record_custom_event("event_sent", {"http_code": res.status_code})
if res.status_code != 200:
LOG.warning(
f"Failed to send event to webhook: {res.status_code} {res.text}"

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)

392
app/poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiohttp"
@ -276,21 +276,6 @@ files = [
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
[[package]]
name = "backports.entry-points-selectable"
version = "1.1.1"
description = "Compatibility shim providing selectable entry points for older implementations"
optional = false
python-versions = ">=2.7"
files = [
{file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"},
{file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"},
]
[package.extras]
docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"]
testing = ["pytest", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"]
[[package]]
name = "bcrypt"
version = "3.2.0"
@ -491,13 +476,13 @@ pycparser = "*"
[[package]]
name = "cfgv"
version = "3.2.0"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.6.1"
python-versions = ">=3.8"
files = [
{file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"},
{file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"},
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
@ -690,6 +675,21 @@ sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
[[package]]
name = "cssbeautifier"
version = "1.15.1"
description = "CSS unobfuscator and beautifier."
optional = false
python-versions = "*"
files = [
{file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
]
[package.dependencies]
editorconfig = ">=0.12.2"
jsbeautifier = "*"
six = ">=1.13.0"
[[package]]
name = "decorator"
version = "4.4.2"
@ -734,41 +734,40 @@ graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "distlib"
version = "0.3.1"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "djlint"
version = "1.3.0"
version = "1.34.1"
description = "HTML Template Linter and Formatter"
optional = false
python-versions = ">=3.7,<4.0"
python-versions = ">=3.8.0,<4.0.0"
files = [
{file = "djlint-1.3.0-py3-none-any.whl", hash = "sha256:0c986bf542cdac3025d431a5b15e6c3977f652f2e76e408dbb5e7aaab6b73d99"},
{file = "djlint-1.3.0.tar.gz", hash = "sha256:b2d8e6c0a14f88da165296f0da05795d15299b7ab0a9093d670ce9ffd867bc79"},
{file = "djlint-1.34.1-py3-none-any.whl", hash = "sha256:96ff1c464fb6f061130ebc88663a2ea524d7ec51f4b56221a2b3f0320a3cfce8"},
{file = "djlint-1.34.1.tar.gz", hash = "sha256:db93fa008d19eaadb0454edf1704931d14469d48508daba2df9941111f408346"},
]
[package.dependencies]
click = ">=8.0.1,<9.0.0"
colorama = ">=0.4.4,<0.5.0"
cssbeautifier = ">=1.14.4,<2.0.0"
html-tag-names = ">=0.1.2,<0.2.0"
html-void-elements = ">=0.1.0,<0.2.0"
importlib-metadata = ">=4.11.0,<5.0.0"
pathspec = ">=0.9.0,<0.10.0"
jsbeautifier = ">=1.14.4,<2.0.0"
json5 = ">=0.9.11,<0.10.0"
pathspec = ">=0.12.0,<0.13.0"
PyYAML = ">=6.0,<7.0"
regex = ">=2022.1.18,<2023.0.0"
regex = ">=2023.0.0,<2024.0.0"
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
tqdm = ">=4.62.2,<5.0.0"
[package.extras]
test = ["coverage (>=6.3.1,<7.0.0)", "pytest (>=7.0.1,<8.0.0)", "pytest-cov (>=3.0.0,<4.0.0)"]
[[package]]
name = "dkimpy"
version = "1.0.5"
@ -806,6 +805,16 @@ doh = ["requests", "requests-toolbelt"]
idna = ["idna (>=2.1)"]
trio = ["sniffio (>=1.1)", "trio (>=0.14.0)"]
[[package]]
name = "editorconfig"
version = "0.12.4"
description = "EditorConfig File Locator and Interpreter for Python"
optional = false
python-versions = "*"
files = [
{file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
]
[[package]]
name = "email-validator"
version = "1.1.3"
@ -851,15 +860,20 @@ requests = "*"
[[package]]
name = "filelock"
version = "3.0.12"
version = "3.15.4"
description = "A platform independent file lock."
optional = false
python-versions = "*"
python-versions = ">=3.8"
files = [
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "flanker"
version = "0.9.11"
@ -1358,7 +1372,6 @@ files = [
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"},
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
@ -1367,7 +1380,6 @@ files = [
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
{file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
@ -1397,7 +1409,6 @@ files = [
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
{file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
@ -1406,7 +1417,6 @@ files = [
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"},
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
@ -1495,17 +1505,17 @@ pyreadline = {version = "*", markers = "sys_platform == \"win32\""}
[[package]]
name = "identify"
version = "1.5.5"
version = "2.6.0"
description = "File identification library for Python"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
python-versions = ">=3.8"
files = [
{file = "identify-1.5.5-py2.py3-none-any.whl", hash = "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"},
{file = "identify-1.5.5.tar.gz", hash = "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4"},
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
]
[package.extras]
license = ["editdistance"]
license = ["ukkonen"]
[[package]]
name = "idna"
@ -1518,25 +1528,6 @@ files = [
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
[[package]]
name = "importlib-metadata"
version = "4.12.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
]
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
name = "iniconfig"
version = "1.0.1"
@ -1669,6 +1660,31 @@ files = [
{file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"},
]
[[package]]
name = "jsbeautifier"
version = "1.15.1"
description = "JavaScript unobfuscator and beautifier."
optional = false
python-versions = "*"
files = [
{file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
]
[package.dependencies]
editorconfig = ">=0.12.2"
six = ">=1.13.0"
[[package]]
name = "json5"
version = "0.9.25"
description = "A Python implementation of the JSON5 data format."
optional = false
python-versions = ">=3.8"
files = [
{file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"},
{file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"},
]
[[package]]
name = "jwcrypto"
version = "0.8"
@ -1959,13 +1975,13 @@ urllib3 = ">=1.7,<2"
[[package]]
name = "nodeenv"
version = "1.5.0"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"},
{file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"},
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
@ -2015,13 +2031,13 @@ testing = ["docopt", "pytest (>=3.0.7)"]
[[package]]
name = "pathspec"
version = "0.9.0"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
python-versions = ">=3.8"
files = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
@ -2117,13 +2133,13 @@ files = [
[[package]]
name = "pre-commit"
version = "2.17.0"
version = "3.8.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.6.1"
python-versions = ">=3.9"
files = [
{file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
{file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
]
[package.dependencies]
@ -2131,8 +2147,7 @@ cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
toml = "*"
virtualenv = ">=20.0.8"
virtualenv = ">=20.10.0"
[[package]]
name = "prompt-toolkit"
@ -2665,85 +2680,104 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
[[package]]
name = "regex"
version = "2022.6.2"
version = "2023.12.25"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "regex-2022.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:042d122f9fee3ceb6d7e3067d56557df697d1aad4ff5f64ecce4dc13a90a7c01"},
{file = "regex-2022.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffef4b30785dc2d1604dfb7cf9fca5dc27cd86d65f7c2a9ec34d6d3ae4565ec2"},
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0afa6a601acf3c0dc6de4e8d7d8bbce4e82f8542df746226cd35d4a6c15e9456"},
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a11cbe8eb5fb332ae474895b5ead99392a4ea568bd2a258ab8df883e9c2bf92"},
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c1f62ee2ba880e221bc950651a1a4b0176083d70a066c83a50ef0cb9b178e12"},
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aba3d13c77173e9bfed2c2cea7fc319f11c89a36fcec08755e8fb169cf3b0df"},
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249437f7f5b233792234aeeecb14b0aab1566280de42dfc97c26e6f718297d68"},
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:179410c79fa86ef318d58ace233f95b87b05a1db6dc493fa29404a43f4b215e2"},
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5e201b1232d81ca1a7a22ab2f08e1eccad4e111579fd7f3bbf60b21ef4a16cea"},
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fdecb225d0f1d50d4b26ac423e0032e76d46a788b83b4e299a520717a47d968c"},
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:be57f9c7b0b423c66c266a26ad143b2c5514997c05dd32ce7ca95c8b209c2288"},
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ed657a07d8a47ef447224ea00478f1c7095065dfe70a89e7280e5f50a5725131"},
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24908aefed23dd065b4a668c0b4ca04d56b7f09d8c8e89636cf6c24e64e67a1e"},
{file = "regex-2022.6.2-cp310-cp310-win32.whl", hash = "sha256:775694cd0bb2c4accf2f1cdd007381b33ec8b59842736fe61bdbad45f2ac7427"},
{file = "regex-2022.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:809bbbbbcf8258049b031d80932ba71627d2274029386f0452e9950bcfa2c6e8"},
{file = "regex-2022.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2b5d983eb0adf2049d41f95205bdc3de4e6cc2350e9c80d4409d3a75229de"},
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4c101746a8dac0401abefa716b357c546e61ea2e3d4a564a9db9eac57ccbce"},
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:166ae7674d0a0e0f8044e7335ba86d0716c9d49465cff1b153f908e0470b8300"},
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5eac5d8a8ac9ccf00805d02a968a36f5c967db6c7d2b747ab9ed782b3b3a28b"},
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f57823f35b18d82b201c1b27ce4e55f88e79e81d9ca07b50ce625d33823e1439"},
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d42e3b7b23473729adbf76103e7df75f9167a5a80b1257ca30688352b4bb2dc"},
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2932e728bee0a634fe55ee54d598054a5a9ffe4cd2be21ba2b4b8e5f8064c2c"},
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:17764683ea01c2b8f103d99ae9de2473a74340df13ce306c49a721f0b1f0eb9e"},
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:2ac29b834100d2c171085ceba0d4a1e7046c434ddffc1434dbc7f9d59af1e945"},
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:f43522fb5d676c99282ca4e2d41e8e2388427c0cf703db6b4a66e49b10b699a8"},
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:9faa01818dad9111dbf2af26c6e3c45140ccbd1192c3a0981f196255bf7ec5e6"},
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:17443f99b8f255273731f915fdbfea4d78d809bb9c3aaf67b889039825d06515"},
{file = "regex-2022.6.2-cp36-cp36m-win32.whl", hash = "sha256:4a5449adef907919d4ce7a1eab2e27d0211d1b255bf0b8f5dd330ad8707e0fc3"},
{file = "regex-2022.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4d206703a96a39763b5b45cf42645776f5553768ea7f3c2c1a39a4f59cafd4ba"},
{file = "regex-2022.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fcd7c432202bcb8b642c3f43d5bcafc5930d82fe5b2bf2c008162df258445c1d"},
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:186c5a4a4c40621f64d771038ede20fca6c61a9faa8178f9e305aaa0c2442a97"},
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:047b2d1323a51190c01b6604f49fe09682a5c85d3c1b2c8b67c1cd68419ce3c4"},
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30637e7fa4acfed444525b1ab9683f714be617862820578c9fd4e944d4d9ad1f"},
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adafe6f2c6d86dbf3313866b61180530ca4dcd0c264932dc8fa1ffb10871d58"},
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67ae3601edf86e15ebe40885e5bfdd6002d34879070be15cf18fc0d80ea24fed"},
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:48dddddce0ea7e7c3e92c1e0c5a28c13ca4dc9cf7e996c706d00479652bff76c"},
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:68e5c641645351eb9eb12c465876e76b53717f99e9b92aea7a2dd645a87aa7aa"},
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8fd5f8ae42f789538bb634bdfd69b9aa357e76fdfd7ad720f32f8994c0d84f1e"},
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:71988a76fcb68cc091e901fddbcac0f9ad9a475da222c47d3cf8db0876cb5344"},
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:4b8838f70be3ce9e706df9d72f88a0aa7d4c1fea61488e06fdf292ccb70ad2be"},
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:663dca677bd3d2e2b5b7d0329e9f24247e6f38f3b740dd9a778a8ef41a76af41"},
{file = "regex-2022.6.2-cp37-cp37m-win32.whl", hash = "sha256:24963f0b13cc63db336d8da2a533986419890d128c551baacd934c249d51a779"},
{file = "regex-2022.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ceff75127f828dfe7ceb17b94113ec2df4df274c4cd5533bb299cb099a18a8ca"},
{file = "regex-2022.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a6f2698cfa8340dfe4c0597782776b393ba2274fe4c079900c7c74f68752705"},
{file = "regex-2022.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8a08ace913c4101f0dc0be605c108a3761842efd5f41a3005565ee5d169fb2b"},
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26dbe90b724efef7820c3cf4a0e5be7f130149f3d2762782e4e8ac2aea284a0b"},
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5f759a1726b995dc896e86f17f9c0582b54eb4ead00ed5ef0b5b22260eaf2d0"},
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fc26bb3415e7aa7495c000a2c13bf08ce037775db98c1a3fac9ff04478b6930"},
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52684da32d9003367dc1a1c07e059b9bbaf135ad0764cd47d8ac3dba2df109bc"},
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c1264eb40a71cf2bff43d6694ab7254438ca19ef330175060262b3c8dd3931a"},
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc635ab319c9b515236bdf327530acda99be995f9d3b9f148ab1f60b2431e970"},
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:27624b490b5d8880f25dac67e1e2ea93dfef5300b98c6755f585799230d6c746"},
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:555f7596fd1f123f8c3a67974c01d6ef80b9769e04d660d6c1a7cc3e6cff7069"},
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:933e72fbe1829cbd59da2bc51ccd73d73162f087f88521a87a8ec9cb0cf10fa8"},
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:cff5c87e941292c97d11dc81bd20679f56a2830f0f0e32f75b8ed6e0eb40f704"},
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c757f3a27b6345de13ef3ca956aa805d7734ce68023e84d0fc74e1f09ce66f7a"},
{file = "regex-2022.6.2-cp38-cp38-win32.whl", hash = "sha256:a58d21dd1a2d6b50ed091554ff85e448fce3fe33a4db8b55d0eba2ca957ed626"},
{file = "regex-2022.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:495a4165172848503303ed05c9d0409428f789acc27050fe2cf0a4549188a7d5"},
{file = "regex-2022.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1ab5cf7d09515548044e69d3a0ec77c63d7b9dfff4afc19653f638b992573126"},
{file = "regex-2022.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1ea28f0ee6cbe4c0367c939b015d915aa9875f6e061ba1cf0796ca9a3010570"},
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de1ecf26ce85521bf73897828b6d0687cc6cf271fb6ff32ac63d26b21f5e764"},
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7c7044aabdad2329974be2246babcc21d3ede852b3971a90fd8c2056c20360"},
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53d69d77e9cfe468b000314dd656be85bb9e96de088a64f75fe128dfe1bf30dd"},
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c8d61883a38b1289fba9944a19a361875b5c0170b83cdcc95ea180247c1b7d3"},
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5429202bef174a3760690d912e3a80060b323199a61cef6c6c29b30ce09fd17"},
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e85b10280cf1e334a7c95629f6cbbfe30b815a4ea5f1e28d31f79eb92c2c3d93"},
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c400dfed4137f32127ea4063447006d7153c974c680bf0fb1b724cce9f8567fc"},
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f648037c503985aed39f85088acab6f1eb6a0482d7c6c665a5712c9ad9eaefc"},
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e7b2ff451f6c305b516281ec45425dd423223c8063218c5310d6f72a0a7a517c"},
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:be456b4313a86be41706319c397c09d9fdd2e5cdfde208292a277b867e99e3d1"},
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c3db393b21b53d7e1d3f881b64c29d886cbfdd3df007e31de68b329edbab7d02"},
{file = "regex-2022.6.2-cp39-cp39-win32.whl", hash = "sha256:d70596f20a03cb5f935d6e4aad9170a490d88fc4633679bf00c652e9def4619e"},
{file = "regex-2022.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:3b9b6289e03dbe6a6096880d8ac166cb23c38b4896ad235edee789d4e8697152"},
{file = "regex-2022.6.2.tar.gz", hash = "sha256:f7b43acb2c46fb2cd506965b2d9cf4c5e64c9c612bac26c1187933c7296bf08c"},
{file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"},
{file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"},
{file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"},
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"},
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"},
{file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"},
{file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"},
{file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"},
{file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"},
{file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"},
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"},
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"},
{file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"},
{file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"},
{file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"},
{file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"},
{file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"},
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"},
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"},
{file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"},
{file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"},
{file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"},
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"},
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"},
{file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"},
{file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"},
{file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"},
{file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"},
{file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"},
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"},
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"},
{file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"},
{file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"},
{file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"},
{file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"},
{file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"},
{file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"},
{file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"},
{file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"},
{file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"},
{file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"},
]
[[package]]
@ -3130,17 +3164,6 @@ idna = "*"
requests = ">=2.1.0"
requests-file = ">=1.4"
[[package]]
name = "toml"
version = "0.10.1"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = "*"
files = [
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
]
[[package]]
name = "tomli"
version = "2.0.1"
@ -3288,25 +3311,23 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "virtualenv"
version = "20.8.1"
version = "20.21.1"
description = "Virtual Python Environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"},
{file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"},
{file = "virtualenv-20.21.1-py3-none-any.whl", hash = "sha256:09ddbe1af0c8ed2bb4d6ed226b9e6415718ad18aef9fa0ba023d96b7a8356049"},
{file = "virtualenv-20.21.1.tar.gz", hash = "sha256:4c104ccde994f8b108163cf9ba58f3d11511d9403de87fb9b4f52bf33dbc8668"},
]
[package.dependencies]
"backports.entry-points-selectable" = ">=1.0.4"
distlib = ">=0.3.1,<1"
filelock = ">=3.0.0,<4"
platformdirs = ">=2,<3"
six = ">=1.9.0,<2"
distlib = ">=0.3.6,<1"
filelock = ">=3.4.1,<4"
platformdirs = ">=2.4,<4"
[package.extras]
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"]
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
[[package]]
name = "watchtower"
@ -3605,21 +3626,6 @@ files = [
idna = ">=2.0"
multidict = ">=4.0"
[[package]]
name = "zipp"
version = "3.2.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.6"
files = [
{file = "zipp-3.2.0-py3-none-any.whl", hash = "sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6"},
{file = "zipp-3.2.0.tar.gz", hash = "sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"},
]
[package.extras]
docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"]
testing = ["func-timeout", "jaraco.itertools", "jaraco.test (>=3.2.0)", "pytest (>=3.5,!=3.7.3)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8", "pytest-mypy"]
[[package]]
name = "zope.event"
version = "4.5.0"
@ -3698,4 +3704,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "01afc410d21eeac0a0ac7e8ef6eeb0a991cf4bc091c3351049263462e205ff63"
content-hash = "22b9a61e9999a215aacb889b3790ee1a6840ce249aea2e3d16c6113243d5c126"

View File

@ -121,13 +121,13 @@ aiospamc = "0.10"
[tool.poetry.dev-dependencies]
pytest = "^7.0.0"
pytest-cov = "^3.0.0"
pre-commit = "^2.17.0"
black = "^22.1.0"
djlint = "^1.3.0"
pylint = "^2.14.4"
[tool.poetry.group.dev.dependencies]
ruff = "^0.1.5"
pre-commit = "^3.8.0"
[build-system]
requires = ["poetry>=0.12"]

View File

@ -2,23 +2,40 @@
{% macro show_user(user) -%}
<h4>User {{ user.email }} with ID {{ user.id }}.</h4>
{% set pu = helper.partner_user(user) %}
<table class="table">
<thead>
<tr>
<th scope="col">User ID</th>
<th scope="col">Email</th>
<th scope="col">Status</th>
<th scope="col">Paid</th>
<th>Subscription</th>
<th>Created At</th>
<th>Updated At</th>
<th>Connected with Proton account</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ user.id }}</td>
<td>{{ user.email }}</td>
<td>{{ "yes" if user.is_paid() else No }}</td>
<td><a href="?email={{ user.email }}">{{ user.email }}</a></td>
{% if user.disabled %}
<td class="text-danger">Disabled</td>
{% else %}
<td class="text-success">Enabled</td>
{% endif %}
<td>{{ "yes" if user.is_paid() else "No" }}</td>
<td>{{ user.get_active_subscription() }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.updated_at }}</td>
{% if pu %}
<td><a href="?email={{ pu.partner_email }}">{{ pu.partner_email }}</a></td>
{% else %}
<td>No</td>
{% endif %}
</tr>
</tbody>
</table>
@ -26,7 +43,7 @@
{% macro list_mailboxes(mbox_count, mboxes) %}
<h4>
{{ mbox_count }} Mailboxes found.
{% if mbox_count>10 %}Showing only the first 10.{% endif %}
{% if mbox_count>10 %}Showing only the last 10.{% endif %}
</h4>
<table class="table">
<thead>
@ -42,9 +59,11 @@
<tr>
<td>{{ mailbox.id }}</td>
<td>{{ mailbox.email }}</td>
<td><a href="?email={{mailbox.email}}">{{mailbox.email}}</a></td>
<td>{{ "Yes" if mailbox.verified else "No" }}</td>
<td>{{ mailbox.created_at }}</td>
<td>
{{ mailbox.created_at }}
</td>
</tr>
{% endfor %}
</tbody>
@ -53,67 +72,54 @@
{% macro list_alias(alias_count, aliases) %}
<h4>
{{ alias_count }} Aliases found.
{% if alias_count>10 %}Showing only the first 10.{% endif %}
{% if alias_count>10 %}Showing only the last 10.{% endif %}
</h4>
<table class="table">
<thead>
<tr>
<th>Alias ID</th>
<th>Email</th>
<th>Verified</th>
<th>Created At</th>
<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><a href="?email={{alias.email}}">{{alias.email}}</a></td>
<td>{{ "Yes" if alias.verified else "No" }}</td>
<td>
{{ alias.created_at }}
</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>
<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>
<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>
<td>{{ deleted_alias.id }}</td>
<td>{{ deleted_alias.email }}</td>
<td>{{ deleted_alias.created_at }}</td>
<td>{{ deleted_alias.reason }}</td>
</tr>
</tbody>
</table>
@ -125,49 +131,23 @@
<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>
<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>
<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>
@ -176,34 +156,26 @@
{% block body %}
<div class="border border-dark border-2 mt-1 mb-2 p-3">
<form method="post">
<form method="get">
<div class="form-group">
<label for="email">
Email to search:
</label>
<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>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% if no_match %}
{% if data.no_match and email %}
<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>
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>
<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) ) }}
@ -212,39 +184,36 @@
{% 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>
<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 %}
{% if data.mailbox_count > 10 %}
<h3>Found more than 10 mailboxes for {{ email }}. Showing the last 10</h3>
{% elif data.mailbox_count > 0 %}
<h3>Found {{ data.mailbox_count }} mailbox(es) for {{ email }}</h3>
{% endif %}
{% for mailbox in 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) }}
<h3 class="mb-3">Found Mailbox {{ mailbox.email }}</h3>
{{ list_mailboxes(1, [mailbox]) }}
{{ show_user(mailbox.user) }}
</div>
{% endif %}
{% endfor %}
{% 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>
<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>
<h3 class="mb-3">Found DomainDeletedAlias {{ data.domain_deleted_alias.email }}</h3>
{{ show_domain_deleted_alias(data.domain_deleted_alias) }}
</div>
{% endif %}

View File

@ -7,7 +7,7 @@
<div class="text-center text-muted small mt-4">
Ask for another activation email?
<a href="{{ url_for('auth.resend_activation') }}" style="color: #4d21ff">Resend</a>
<a href="{{ url_for("auth.resend_activation") }}" style="color: #4d21ff">Resend</a>
</div>
{% endif %}
{% endblock %}

View File

@ -13,7 +13,7 @@
</div>
<div class="text-center">
Please go to
<a href="{{ url_for('dashboard.setting') }}">settings</a>
<a href="{{ url_for("dashboard.setting") }}">settings</a>
page to re-send the confirmation email.
</div>
</div>

View File

@ -33,7 +33,7 @@
<div class="text-muted mt-5" style="margin-top: 1em;">
Don't have your key with you?
<br />
<a href="{{ url_for('auth.mfa') }}">Verify by One-Time Password</a>
<a href="{{ url_for("auth.mfa") }}">Verify by One-Time Password</a>
</div>
{% endif %}
<hr />

View File

@ -20,7 +20,7 @@
</form>
<div class="text-center text-muted">
Forget it,
<a href="{{ url_for('auth.login') }}">send me back</a>
<a href="{{ url_for("auth.login") }}">send me back</a>
to the sign in screen.
</div>
{% endblock %}

View File

@ -7,7 +7,7 @@
<div class="text-center text-muted small mb-4">
You haven't received the activation email?
<a href="{{ url_for('auth.resend_activation') }}">Resend</a>
<a href="{{ url_for("auth.resend_activation") }}">Resend</a>
</div>
{% endif %}
<div class="card" style="border-radius: 2%">
@ -25,7 +25,7 @@
{{ form.password(class="form-control", type="password") }}
{{ render_field_errors(form.password) }}
<div class="text-muted">
<a href="{{ url_for('auth.forgot_password') }}" class="small">I forgot my password</a>
<a href="{{ url_for("auth.forgot_password") }}" class="small">I forgot my password</a>
</div>
</div>
<div class="form-footer">
@ -57,6 +57,6 @@
</div>
<div class="text-center text-muted mt-2">
Don't have an account yet?
<a href="{{ url_for('auth.register') }}">Sign up</a>
<a href="{{ url_for("auth.register") }}">Sign up</a>
</div>
{% endblock %}

View File

@ -30,7 +30,7 @@
<div class="text-muted mt-5" style="margin-top: 1em;">
Having trouble with your authenticator?
<br />
<a href="{{ url_for('auth.fido') }}">
<a href="{{ url_for("auth.fido") }}">
Verify by your security
key
</a>

View File

@ -69,6 +69,6 @@
</form>
<div class="text-center text-muted mb-6">
Already have account?
<a href="{{ url_for('auth.login') }}">Sign in</a>
<a href="{{ url_for("auth.login") }}">Sign in</a>
</div>
{% endblock %}

View File

@ -19,6 +19,6 @@
</form>
<div class="text-center text-muted">
Don't have account yet?
<a href="{{ url_for('auth.register') }}">Sign up</a>
<a href="{{ url_for("auth.register") }}">Sign up</a>
</div>
{% endblock %}

View File

@ -29,7 +29,9 @@
{% endif %}
</div>
<div class="text-center p-3"
style="font-size: 12px; font-weight: 300; margin: auto">
style="font-size: 12px;
font-weight: 300;
margin: auto">
<span class="badge badge-warning">Warning</span>
Please note that social login is now <b>deprecated</b>.
<br />
@ -39,8 +41,8 @@
</div>
</div>
<div class="text-center text-muted mt-2">
<a href="{{ url_for('auth.register') }}">Sign up</a>
<a href="{{ url_for("auth.register") }}">Sign up</a>
/
<a href="{{ url_for('auth.login') }}">Login</a>
<a href="{{ url_for("auth.login") }}">Login</a>
</div>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% from "_formhelpers.html" import render_field, render_field_errors %}
<!doctype html>
<!DOCTYPE html>
<html lang="en"
dir="ltr"
data-theme="{%- if request.cookies.get('dark-mode') == 'true' -%} dark{%- endif -%}">

View File

@ -33,7 +33,7 @@
This email address is used to log in to SimpleLogin.
<br />
If you want to change the mailbox that emails are forwarded to, use the
<a href="{{ url_for('dashboard.mailbox_route') }}">
<a href="{{ url_for("dashboard.mailbox_route") }}">
<i class="fe fe-inbox"></i> Mailboxes page
</a>
instead.
@ -50,14 +50,14 @@
<div class="mt-2">
<span class="text-danger float-left">Pending email change: {{ pending_email }}</span>
<form method="POST"
action="{{ url_for('dashboard.resend_email_change') }}"
action="{{ url_for("dashboard.resend_email_change") }}"
class="float-left ml-2">
{{ change_email_form.csrf_token }}
<a onclick="this.closest('form').submit()"
class="btn btn-secondary btn-sm">Resend confirmation email</a>
</form>
<form method="POST"
action="{{ url_for('dashboard.cancel_email_change') }}"
action="{{ url_for("dashboard.cancel_email_change") }}"
class="float-left ml-2">
{{ change_email_form.csrf_token }}
<a onclick="this.closest('form').submit()"
@ -91,10 +91,10 @@
</div>
{% if not current_user.enable_otp %}
<a href="{{ url_for('dashboard.mfa_setup') }}"
<a href="{{ url_for("dashboard.mfa_setup") }}"
class="btn btn-outline-primary">Setup TOTP</a>
{% else %}
<a href="{{ url_for('dashboard.mfa_cancel') }}"
<a href="{{ url_for("dashboard.mfa_cancel") }}"
class="btn btn-outline-danger">Disable TOTP</a>
{% endif %}
</div>
@ -111,10 +111,10 @@
</div>
{% if current_user.fido_uuid is none %}
<a href="{{ url_for('dashboard.fido_setup') }}"
<a href="{{ url_for("dashboard.fido_setup") }}"
class="btn btn-outline-primary">Setup WebAuthn</a>
{% else %}
<a href="{{ url_for('dashboard.fido_manage') }}"
<a href="{{ url_for("dashboard.fido_manage") }}"
class="btn btn-outline-info">Manage WebAuthn</a>
{% endif %}
</div>
@ -146,7 +146,7 @@
<div class="card-body">
<div class="card-title">Account Deletion</div>
<div class="mb-3">If SimpleLogin isn't the right fit for you, you can simply delete your account.</div>
<a href="{{ url_for('dashboard.delete_account') }}"
<a href="{{ url_for("dashboard.delete_account") }}"
class="btn btn-outline-danger">Delete account</a>
</div>
</div>

View File

@ -75,9 +75,7 @@
{% else %}
<button disabled
title="Upgrade to premium to create reverse-aliases"
class="btn btn-primary mt-2">
Create reverse-alias
</button>
class="btn btn-primary mt-2">Create reverse-alias</button>
{% endif %}
</form>
</div>
@ -98,9 +96,7 @@
{% if highlight_contact_id %}
<a href="{{ url_for("dashboard.alias_contact_manager", alias_id=alias.id, highlight_contact_id=highlight_contact_id) }}"
class="btn btn-light">
Reset
</a>
class="btn btn-light">Reset</a>
{% else %}
<a href="{{ url_for("dashboard.alias_contact_manager", alias_id=alias.id) }}"
class="btn btn-light">Reset</a>
@ -139,15 +135,11 @@
target="_blank"
data-toggle="tooltip"
title="You can click on this to open your email client. Or use the copy button 👉"
class="font-weight-bold">
*************************
</a>
class="font-weight-bold">*************************</a>
<span class="clipboard btn btn-sm btn-success copy-btn"
data-toggle="tooltip"
title="Copy the reverse-alias to clipboard"
data-clipboard-text="{{ contact.website_send_to() }}">
Copy reverse-alias
</span>
data-clipboard-text="{{ contact.website_send_to() }}">Copy reverse-alias</span>
</span>
</div>
<div class="mb-2 text-muted small-text">
@ -207,14 +199,12 @@
<nav aria-label="Contact navigation">
<ul class="pagination">
<li class="page-item">
<a class="btn btn-outline-secondary {% if page == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page-1) }}">
<a class="btn btn-outline-secondary {% if page == 0 %}disabled{% endif %}" href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page-1) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page+1) }}">
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}" href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page+1) }}">
Next
</a>
</li>

View File

@ -13,7 +13,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Total</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ total }}</div>
</div>
@ -25,7 +27,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Forwarded</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ email_forwarded }}</div>
</div>
@ -37,7 +41,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Replies/Sent</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ email_replied }}</div>
</div>
@ -49,7 +55,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Blocked</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ email_blocked }}</div>
</div>
@ -111,14 +119,12 @@
<nav aria-label="Alias log navigation">
<ul class="pagination">
<li class="page-item">
<a class="btn btn-outline-secondary {% if page_id == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id-1) }}">
<a class="btn btn-outline-secondary {% if page_id == 0 %}disabled{% endif %}" href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id-1) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id+1) }}">
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}" href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id+1) }}">
Next
</a>
</li>

View File

@ -15,10 +15,7 @@
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
{% endfor %}
</select>
<button class="btn btn-success mt-2">Confirm</button>

View File

@ -16,9 +16,7 @@
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ alias_transfer_url }}">
{{ alias_transfer_url }}
</em>
data-clipboard-text="{{ alias_transfer_url }}">{{ alias_transfer_url }}</em>
<p class="mt-5">
Please copy the transfer URL. <strong>We won't be able to display it again</strong>. If you need to access it again you can generate a new URL.
</p>

View File

@ -22,7 +22,7 @@
<br />
The period left in the current subscription isn't taken into account.
<br />
<a href="{{ url_for('dashboard.pricing') }}"
<a href="{{ url_for("dashboard.pricing") }}"
class="btn btn-primary mt-2">Re-subscribe</a>
</p>
{% else %}

View File

@ -43,12 +43,14 @@
{% endif %}
<div class="form-group">
<label class="form-label">PGP Public Key</label>
<textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="(Drag and drop or paste your pgp public key here)&#10;-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ contact.pgp_public_key or "" }}</textarea>
<textarea name="pgp"
{% if not current_user.is_premium() %}disabled{% endif %}
class="form-control"
rows="10"
id="pgp-public-key"
placeholder="(Drag and drop or paste your pgp public key here)&#10;-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ contact.pgp_public_key or "" }}</textarea>
</div>
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}
disabled {% endif %} value="save">
Save
</button>
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}disabled{% endif %} value="save">Save</button>
{% if contact.pgp_finger_print %}
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>

View File

@ -74,10 +74,7 @@
required>
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
{% endfor %}
</select>
<div class="small-text">The mailbox(es) that owns this alias.</div>
@ -102,7 +99,6 @@
</div>
{% endblock %}
{% block script %}
<script>
$('.mailbox-select').multipleSelect();

View File

@ -30,9 +30,7 @@
</a>
</div>
{% endif %}
<div class="alert alert-primary collapse {% if not custom_domains %} show{% endif %}"
id="howtouse"
role="alert">
<div class="alert alert-primary collapse {% if not custom_domains %}show{% endif %}" id="howtouse" role="alert">
By adding your domain, you can create aliases like <b>hi@my-domain.com</b>
<br />
You can also enable <b>catch-all</b> to create aliases on-the-fly:
@ -50,18 +48,14 @@
{% if custom_domain.ownership_verified and not custom_domain.verified %}
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id, _anchor='dns-setup') }}"
class="btn btn-info btn-sm">
Ownership verified. Setup the DNS
</a>
class="btn btn-info btn-sm">Ownership verified. Setup the DNS</a>
{% elif custom_domain.ownership_verified and custom_domain.verified %}
<span class="badge badge-success">Domain ready</span>
<!-- custom_domain.ownership_verified is False -->
{% else %}
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id, _anchor='ownership-form') }}"
class="btn btn-warning btn-sm"
role="button">
Verify domain ownership
</a>
role="button">Verify domain ownership</a>
{% endif %}
</h5>
<h6 class="card-subtitle mb-4 text-muted">

View File

@ -22,9 +22,7 @@
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
{% endif %}
<div class="alert alert-primary collapse {% if not dirs %} show{% endif %}"
id="howtouse"
role="alert">
<div class="alert alert-primary collapse {% if not dirs %}show{% endif %}" id="howtouse" role="alert">
<div>
Directory allows you to create aliases <b>on the fly</b>.
</div>
@ -103,10 +101,7 @@
name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox in dir_mailboxes %} selected{% endif %}>
{{ mailbox.email }}
</option>
<option value="{{ mailbox.id }}" {% if mailbox in dir_mailboxes %}selected{% endif %}>{{ mailbox.email }}</option>
{% endfor %}
</select>
<button class="mt-2 btn btn-outline-primary btn-sm">Update</button>
@ -156,10 +151,7 @@
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
{% endfor %}
</select>
<button id="btn-create-directory" class="btn btn-primary mt-2">Create</button>

View File

@ -13,7 +13,8 @@
<div class="alert alert-warning mt-3">Rules are ineffective when catch-all is enabled.</div>
{% endif %}
<div class="{% if custom_domain.catch_all %} disabled-content{% endif %}">
<div class="{% if custom_domain.catch_all %}
disabled-content{% endif %}">
<div class="mt-3 mb-2">
For a greater control than a simple catch-all, you can define a set of <b>rules</b> to auto create aliases.
<br />
@ -60,8 +61,7 @@
<div class="form-group">
<label>Regex</label>
{{ new_auto_create_rule_form.regex(class="form-control",
placeholder="prefix.*"
) }}
placeholder="prefix.*") }}
{{ render_field_errors(new_auto_create_rule_form.regex) }}
<div class="small-text">
For example, if you want aliases that starts with <b>prefix</b> to be automatically created, you can set
@ -95,10 +95,7 @@
name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
{% endfor %}
</select>
</div>
@ -128,9 +125,7 @@
{% if auto_create_test_result %}
<div class="alert {% if auto_create_test_passed %}
alert-success {% else %} alert-warning {% endif %}">
{{ auto_create_test_result }}
</div>
alert-success {% else %} alert-warning {% endif %}">{{ auto_create_test_result }}</div>
{% endif %}
</div>
</div>

View File

@ -63,8 +63,8 @@
{% endif %}
<hr />
{% endif %}
<div class="{% if not custom_domain.ownership_verified %} disabled-content{% endif %}"
id="dns-setup">
<div class="{% if not custom_domain.ownership_verified %}
disabled-content{% endif %}" id="dns-setup">
{% if not custom_domain.ownership_verified %}
<div class="alert alert-warning">A domain ownership must be verified first.</div>
@ -177,9 +177,7 @@
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ spf_record }}">
{{ spf_record }}
</em>
data-clipboard-text="{{ spf_record }}">{{ spf_record }}</em>
</div>
<form method="post" action="#spf-form">
{{ csrf_form.csrf_token }}
@ -238,9 +236,7 @@
Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam
folder.
</div>
<div class="mb-2">
Add the following CNAME DNS records to your domain.
</div>
<div class="mb-2">Add the following CNAME DNS records to your domain.</div>
{% for dkim_prefix, dkim_cname_value in dkim_records %}
<div class="mb-2 p-3 dns-record">
@ -256,9 +252,7 @@
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ dkim_cname_value }}."
style="overflow-wrap: break-word">
{{ dkim_cname_value }}.
</em>
style="overflow-wrap: break-word">{{ dkim_cname_value }}.</em>
</div>
{% endfor %}
<div class="alert alert-info">
@ -282,21 +276,15 @@
<input type="hidden" name="form-name" value="check-dkim">
{% if custom_domain.dkim_verified %}
<button type="submit" class="btn btn-outline-primary">
Re-verify
</button>
<button type="submit" class="btn btn-outline-primary">Re-verify</button>
{% else %}
<button type="submit" class="btn btn-primary">
Verify
</button>
<button type="submit" class="btn btn-primary">Verify</button>
{% endif %}
</form>
{% if not dkim_ok %}
<div class="text-danger mt-4">
<p>
Your DNS is not correctly set.
</p>
<p>Your DNS is not correctly set.</p>
<ul>
{% for custom_record, retrieved_cname in dkim_errors.items() %}
@ -312,9 +300,7 @@
</div>
{% if custom_domain.dkim_verified %}
<div class="text-danger mt-4">
DKIM is still enabled. Please update your DKIM settings with all CNAME records
</div>
<div class="text-danger mt-4">DKIM is still enabled. Please update your DKIM settings with all CNAME records</div>
{% endif %}
{% endif %}
</div>
@ -337,17 +323,13 @@
DMARC
<a href="https://en.wikipedia.org/wiki/DMARC"
target="_blank"
rel="noopener noreferrer">
(Wikipedia↗)
</a>
rel="noopener noreferrer">(Wikipedia↗)</a>
is designed to protect the domain from unauthorized use, commonly known as email spoofing.
<br />
Built around SPF and DKIM, a DMARC policy tells the receiving mail server what to do if
neither of those authentication methods passes.
</div>
<div class="mb-2">
Add the following TXT DNS record to your domain.
</div>
<div class="mb-2">Add the following TXT DNS record to your domain.</div>
<div class="mb-2 p-3 dns-record">
Record: TXT
<br />
@ -360,9 +342,7 @@
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ dmarc_record }}">
{{ dmarc_record }}
</em>
data-clipboard-text="{{ dmarc_record }}">{{ dmarc_record }}</em>
</div>
<div class="alert alert-info">
Some DNS registrar might require a full record path, in this case please use
@ -377,13 +357,9 @@
<input type="hidden" name="form-name" value="check-dmarc">
{% if custom_domain.dmarc_verified %}
<button type="submit" class="btn btn-outline-primary">
Re-verify
</button>
<button type="submit" class="btn btn-outline-primary">Re-verify</button>
{% else %}
<button type="submit" class="btn btn-primary">
Verify
</button>
<button type="submit" class="btn btn-primary">Verify</button>
{% endif %}
</form>
{% if not dmarc_ok %}

View File

@ -34,7 +34,8 @@
.
</div>
</div>
<div class="{% if not custom_domain.catch_all %} disabled-content{% endif %}">
<div class="{% if not custom_domain.catch_all %}
disabled-content{% endif %}">
<div>
Auto-created aliases are automatically owned by the following mailboxes
<i class="fe fe-corner-right-down"></i>
@ -54,10 +55,7 @@
name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox in domain_mailboxes %} selected{% endif %}>
{{ mailbox.email }}
</option>
<option value="{{ mailbox.id }}" {% if mailbox in domain_mailboxes %}selected{% endif %}>{{ mailbox.email }}</option>
{% endfor %}
</select>
</div>

View File

@ -45,7 +45,7 @@
<td>Link a New Key</td>
<td></td>
<td class="text-center">
<a href="{{ url_for('dashboard.fido_setup') }}">
<a href="{{ url_for("dashboard.fido_setup") }}">
<button class="btn btn-outline-success">Link</button>
</a>
</td>

View File

@ -61,8 +61,7 @@
class="btn btn-success dropdown-toggle btn-group-border-left"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
</button>
aria-expanded="false"></button>
<div class="dropdown-menu dropdown-menu-right border-left"
aria-labelledby="btnGroupDrop1">
<div>
@ -125,7 +124,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Aliases</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">All time</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">All time</div>
</div>
<div class="h1 m-0">{{ stats.nb_alias }}</div>
</div>
@ -137,7 +138,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Forwarded</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ stats.nb_forward }}</div>
</div>
@ -149,7 +152,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Replies/Sent</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ stats.nb_reply }}</div>
</div>
@ -161,7 +166,9 @@
<div class="d-flex align-items-center">
<div class="subheader">Blocked</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
style="order: 2;
margin-left: auto;
font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ stats.nb_block }}</div>
</div>
@ -177,52 +184,28 @@
<select name="sort"
onchange="this.form.submit()"
class="form-control mr-3 shadow">
<option value="" {% if sort == "" %} selected{% endif %}>
Sort by most recent activity
</option>
<option value="old2new" {% if sort == "old2new" %} selected{% endif %}>
Alias Old-Recent
</option>
<option value="new2old" {% if sort == "new2old" %} selected{% endif %}>
Alias Recent-Old
</option>
<option value="a2z" {% if sort == "a2z" %} selected{% endif %}>
Alias A-Z
</option>
<option value="z2a" {% if sort == "z2a" %} selected{% endif %}>
Alias Z-A
</option>
<option value="" {% if sort == "" %}selected{% endif %}>Sort by most recent activity</option>
<option value="old2new" {% if sort == "old2new" %}selected{% endif %}>Alias Old-Recent</option>
<option value="new2old" {% if sort == "new2old" %}selected{% endif %}>Alias Recent-Old</option>
<option value="a2z" {% if sort == "a2z" %}selected{% endif %}>Alias A-Z</option>
<option value="z2a" {% if sort == "z2a" %}selected{% endif %}>Alias Z-A</option>
</select>
<select name="filter"
onchange="this.form.submit()"
class="form-control mr-3 shadow"
style="max-width: 200px">
<option value="" {% if filter == "" %} selected{% endif %}>
All Aliases
</option>
<option value="pinned" {% if filter == "pinned" %} selected{% endif %}>
Pinned Aliases
</option>
<option value="enabled" {% if filter == "enabled" %} selected{% endif %}>
Only Enabled Aliases
</option>
<option value="disabled" {% if filter == "disabled" %} selected{% endif %}>
Only Disabled Aliases
</option>
<option value="hibp" {% if filter == "hibp" %} selected{% endif %}>
Only Aliases Found In Data Breaches
</option>
<option value="" {% if filter == "" %}selected{% endif %}>All Aliases</option>
<option value="pinned" {% if filter == "pinned" %}selected{% endif %}>Pinned Aliases</option>
<option value="enabled" {% if filter == "enabled" %}selected{% endif %}>Only Enabled Aliases</option>
<option value="disabled" {% if filter == "disabled" %}selected{% endif %}>Only Disabled Aliases</option>
<option value="hibp" {% if filter == "hibp" %}selected{% endif %}>Only Aliases Found In Data Breaches</option>
{% for mailbox in current_user.mailboxes() %}
<option value="mailbox:{{ mailbox.id }}" {% if filter == "mailbox:" ~ mailbox.id %}
selected {% endif %}>
{{ mailbox.email }}'s aliases
</option>
<option value="mailbox:{{ mailbox.id }}" {% if filter == "mailbox:" ~ mailbox.id %}selected{% endif %}>{{ mailbox.email }}'s aliases</option>
{% endfor %}
{% for directory in current_user.directories %}
<option value="directory:{{ directory.id }}" {% if filter == "directory:" ~ directory.id %}
selected {% endif %}>
<option value="directory:{{ directory.id }}" {% if filter == "directory:" ~ directory.id %}selected{% endif %}>
Directory <b>{{ directory.name }}</b> aliases
</option>
{% endfor %}
@ -237,7 +220,7 @@
<div style="margin-left: auto">
{% if query or sort or filter %}
<a href="{{ url_for('dashboard.index') }}"
<a href="{{ url_for("dashboard.index") }}"
class="btn btn-outline-secondary">Reset</a>
{% endif %}
</div>
@ -254,7 +237,8 @@
<div class="card p-4 shadow-sm {% if alias.id == highlight_alias_id %}highlight-row{% endif %} ">
<div class="row">
<div class="col-8">
<span class="{% if alias.id == highlight_alias_id %} highlighted{% endif %} clipboard cursor mb-0" {% if loop.index ==1 %}
<span class="{% if alias.id == highlight_alias_id %}
highlighted{% endif %} clipboard cursor mb-0" {% if loop.index ==1 %}
data-intro="This is your first <em>alias</em>.
<br />
<br />
@ -358,7 +342,15 @@
</div>
<div class="d-flex mb-2">
<div class="flex-grow-1 mr-2">
<textarea id="note-{{ alias.id }}" name="note" class="form-control" style="font-size: 12px" rows="2" placeholder="e.g. where the alias is used or why is it created" onchange="handleNoteChange({{ alias.id }}, '{{ alias.email }}')" onfocus="handleNoteFocus({{ alias.id }})" onblur="handleNoteBlur({{ alias.id }})">{{ alias.note or "" }}</textarea>
<textarea id="note-{{ alias.id }}"
name="note"
class="form-control"
style="font-size: 12px"
rows="2"
placeholder="e.g. where the alias is used or why is it created"
onchange="handleNoteChange({{ alias.id }}, '{{ alias.email }}')"
onfocus="handleNoteFocus({{ alias.id }})"
onblur="handleNoteBlur({{ alias.id }})">{{ alias.note or "" }}</textarea>
</div>
</div>
<!-- Send Email && More button -->
@ -399,27 +391,21 @@
</div>
<!-- END Send Email && More button -->
<!-- Collapse section -->
<div class="{% if not current_user.expand_alias_info %} collapse{% endif %} mt-2"
id="alias-{{ alias.id }}">
<div class="{% if not current_user.expand_alias_info %}
collapse{% endif %} mt-2" id="alias-{{ alias.id }}">
{% if alias_info.latest_email_log != None %}
<div style="font-size: 12px">
Alias created {{ alias.created_at | dt }}
</div>
<div style="font-size: 12px">Alias created {{ alias.created_at | dt }}</div>
{% endif %}
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwarded,
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocked,
<span class="alias-activity">{{ alias_info.nb_reply }}</span> sent
in the last 14 days
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
class="btn btn-sm btn-link">
See All &nbsp;
</a>
class="btn btn-sm btn-link">See All &nbsp;</a>
{% if mailboxes|length > 1 %}
<div class="small-text">
Current mailbox
</div>
<div class="small-text">Current mailbox</div>
<div class="d-flex">
<div class="flex-grow-1 mr-2">
<select required
@ -431,10 +417,7 @@
onchange="handleMailboxChange({{ alias.id }}, '{{ alias.email }}')">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}" {% if alias_info.contain_mailbox(mailbox.id) %}
selected {% endif %}>
{{ mailbox.email }}
</option>
<option value="{{ mailbox.id }}" {% if alias_info.contain_mailbox(mailbox.id) %}selected{% endif %}>{{ mailbox.email }}</option>
{% endfor %}
</select>
</div>
@ -502,11 +485,7 @@
<input type="hidden" name="form-name" value="delete-alias">
<input type="hidden" name="alias-id" value="{{ alias.id }}">
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
<span class="btn btn-link btn-sm float-right text-danger"
onclick="confirmDeleteAlias.call(this)"
{% if alias.custom_domain %} data-custom-domain-trash-url="{{ alias.custom_domain.get_trash_url() }}"{% endif %}
data-alias="{{ alias.id }}"
data-alias-email="{{ alias.email }}">
<span class="btn btn-link btn-sm float-right text-danger" onclick="confirmDeleteAlias.call(this)" {% if alias.custom_domain %}data-custom-domain-trash-url="{{ alias.custom_domain.get_trash_url() }}"{% endif %} data-alias="{{ alias.id }}" data-alias-email="{{ alias.email }}">
Delete&nbsp; &nbsp;<i class="dropdown-icon fe fe-trash-2 text-danger"></i>
</span>
</form>
@ -527,14 +506,12 @@
<nav aria-label="Alias navigation">
<ul class="pagination">
<li class="page-item mr-1">
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.index', page=page-1, query=query, sort=sort, filter=filter) }}">
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}" href="{{ url_for('dashboard.index', page=page-1, query=query, sort=sort, filter=filter) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.index', page=page+1, query=query, sort=sort, filter=filter) }}">
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}" href="{{ url_for('dashboard.index', page=page+1, query=query, sort=sort, filter=filter) }}">
Next
</a>
</li>

View File

@ -22,9 +22,7 @@
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
{% endif %}
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %} show{% endif %}"
id="howtouse"
role="alert">
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %}show{% endif %}" id="howtouse" role="alert">
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose
the
mailbox that <em>owns</em> this alias, i.e:
@ -92,9 +90,7 @@
<input type="hidden" name="form-name" value="set-default">
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
<button class="card-link btn btn-link {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
Set As Default Mailbox
</button>
<button class="card-link btn btn-link {% if mailbox.id == current_user.default_mailbox_id %}disabled{% endif %}">Set As Default Mailbox</button>
</form>
</div>
{% endif %}
@ -105,22 +101,16 @@
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
<select hidden name="transfer_mailbox_id" value="">
<option value="-1">
Delete my aliases
</option>
<option value="-1">Delete my aliases</option>
{% for mailbox_opt in mailboxes %}
{% if mailbox_opt.verified and mailbox_opt.id != mailbox.id %}
<option value="{{ mailbox_opt.id }}">
{{ mailbox_opt.email }}
</option>
<option value="{{ mailbox_opt.id }}">{{ mailbox_opt.email }}</option>
{% endif %}
{% endfor %}
</select>
<span class="card-link btn btn-link text-danger float-right delete-mailbox {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
Delete
</span>
<span class="card-link btn btn-link text-danger float-right delete-mailbox {% if mailbox.id == current_user.default_mailbox_id %}disabled{% endif %}">Delete</span>
</form>
</div>
</div>

View File

@ -60,9 +60,7 @@
<div class="mt-2">
<span class="text-danger">Pending change: {{ pending_email }}</span>
<a href="{{ url_for('dashboard.cancel_mailbox_change_route', mailbox_id=mailbox.id) }}"
class="btn btn-secondary btn-sm">
Cancel mailbox change
</a>
class="btn btn-secondary btn-sm">Cancel mailbox change</a>
</div>
{% endif %}
</div>
@ -123,13 +121,15 @@
{{ csrf_form.csrf_token }}
<div class="form-group">
<label class="form-label">PGP Public Key</label>
<textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="(Drag and drop or paste your pgp public key here)&#10;-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ mailbox.pgp_public_key or "" }}</textarea>
<textarea name="pgp"
{% if not current_user.is_premium() %}disabled{% endif %}
class="form-control"
rows="10"
id="pgp-public-key"
placeholder="(Drag and drop or paste your pgp public key here)&#10;-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ mailbox.pgp_public_key or "" }}</textarea>
</div>
<input type="hidden" name="form-name" value="pgp">
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}
disabled {% endif %} value="save">
Save
</button>
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}disabled{% endif %} value="save">Save</button>
{% if mailbox.pgp_finger_print %}
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>

View File

@ -8,7 +8,7 @@
Mailbox <b>{{ mailbox.email }}</b> verified, you can now start creating alias with it
</div>
<div class="mx-auto">
<a href="{{ url_for('dashboard.index') }}" class="btn btn-primary">Go To Home Page</a>
<a href="{{ url_for("dashboard.index") }}" class="btn btn-primary">Go To Home Page</a>
</div>
</div>
{% endblock %}

View File

@ -17,13 +17,9 @@
white-space: normal;
overflow: hidden;
max-height: 100px;
text-overflow: ellipsis;">
{{ notification.message | safe }}
</div>
text-overflow: ellipsis">{{ notification.message | safe }}</div>
<a href="{{ url_for('dashboard.notification_route', notification_id=notification.id) }}"
class="mt-2 btn btn-outline-primary">
More ➡
</a>
class="mt-2 btn btn-outline-primary">More ➡</a>
<div class="small text-muted mt-2">{{ notification.created_at | dt }}</div>
</div>
</div>
@ -36,16 +32,12 @@
<nav aria-label="Notification navigation">
<ul class="pagination">
<li class="page-item mr-1">
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.notifications_route', page=page-1) }}">
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}" href="{{ url_for('dashboard.notifications_route', page=page-1) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.notifications_route', page=page+1) }}">
Next
</a>
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}" href="{{ url_for('dashboard.notifications_route', page=page+1) }}">Next</a>
</li>
</ul>
</nav>

View File

@ -144,9 +144,7 @@
{% set sub = current_user.get_paddle_subscription() %}
<button class="{{ 'invisible' if sub or manual_sub or coinbase_sub }} btn btn-lg btn-outline-secondary w-100 btn-no-pointer"
aria-disabled="true"
disabled>
Current plan
</button>
disabled>Current plan</button>
</div>
</div>
<ul class="list-unstyled">
@ -172,9 +170,7 @@
<div class="h3 my-3">$4 / month</div>
<div class="text-center mt-4 mb-6">
<button class="btn btn-primary btn-lg w-100"
onclick="upgradePaddle({{ PADDLE_MONTHLY_PRODUCT_ID }})">
Upgrade to Premium
</button>
onclick="upgradePaddle({{ PADDLE_MONTHLY_PRODUCT_ID }})">Upgrade to Premium</button>
</div>
</div>
<ul class="list-unstyled">
@ -287,9 +283,7 @@
{% set sub = current_user.get_paddle_subscription() %}
<button class="{{ 'invisible' if sub or manual_sub or coinbase_sub }} btn btn-lg btn-outline-secondary w-100 btn-no-pointer"
aria-disabled="true"
disabled>
Current plan
</button>
disabled>Current plan</button>
</div>
</div>
<ul class="list-unstyled">
@ -315,9 +309,7 @@
<div class="h3 my-3">$30 / year</div>
<div class="text-center mt-4 mb-6">
<button class="btn btn-primary btn-lg w-100"
onclick="upgradePaddle({{ PADDLE_YEARLY_PRODUCT_ID }})">
Upgrade to Premium
</button>
onclick="upgradePaddle({{ PADDLE_YEARLY_PRODUCT_ID }})">Upgrade to Premium</button>
</div>
</div>
<ul class="list-unstyled">
@ -447,18 +439,10 @@
We use <a href="https://paddle.com" target="_blank" rel="noopener noreferrer">Paddle <i class="fe fe-external-link"></i></a> by default for handling payments via credit cards and PayPal. Paddle currently supports the following payment methods:
</p>
<ul>
<li>
Cards (including Mastercard, Visa, Maestro, American Express, Discover, Diners Club, JCB, UnionPay, and Mada)
</li>
<li>
PayPal
</li>
<li>
Apple Pay
</li>
<li>
Wire Transfers (ACH/SEPA/BACS)
</li>
<li>Cards (including Mastercard, Visa, Maestro, American Express, Discover, Diners Club, JCB, UnionPay, and Mada)</li>
<li>PayPal</li>
<li>Apple Pay</li>
<li>Wire Transfers (ACH/SEPA/BACS)</li>
</ul>
<p>
More information can be found on
@ -482,7 +466,7 @@
</p>
<div class="d-flex justify-content-center">
<a class="btn btn-outline-primary text-center"
href="{{ url_for('dashboard.coinbase_checkout_route') }}"
href="{{ url_for("dashboard.coinbase_checkout_route") }}"
target="_blank"
rel="noopener noreferrer">
Upgrade to Premium - cryptocurrency
@ -520,7 +504,7 @@
<div class="card-body">
<p>
To redeem or buy a coupon, please go to the
<a href="{{ url_for('dashboard.coupon_route') }}">coupon page</a>. The coupon code can be used by you or given to someone as a gift.
<a href="{{ url_for("dashboard.coupon_route") }}">coupon page</a>. The coupon code can be used by you or given to someone as a gift.
</p>
</div>
</div>
@ -554,18 +538,12 @@
sending emails. Concretely:
</p>
<ul>
<li>
All aliases/domains/directories/mailboxes you have created are kept and continue working normally.
</li>
<li>
You cannot create new aliases if you exceed the free plan limit, i.e. have more than 10 aliases.
</li>
<li>All aliases/domains/directories/mailboxes you have created are kept and continue working normally.</li>
<li>You cannot create new aliases if you exceed the free plan limit, i.e. have more than 10 aliases.</li>
<li>
As features like catch-all or directory allow you to create aliases on-the-fly, those aliases cannot be automatically created if you have more than 10 aliases.
</li>
<li>
You cannot add new domain, directory or mailbox.
</li>
<li>You cannot add new domain, directory or mailbox.</li>
</ul>
<p>
For example, if you have 100 aliases by the time your subscription ends, these 100 aliases will continue receiving and sending emails normally. You cannot however create new aliases.
@ -630,19 +608,11 @@
aria-labelledby="pricing-faq-question-discounts"
data-parent="#pricing-faq">
<div class="card-body">
<p>
We offer important discounts or free premium for:
</p>
<p>We offer important discounts or free premium for:</p>
<ul>
<li>
students, professors or technical staffs working at an educational institute
</li>
<li>
activists, dissidents or journalists
</li>
<li>
charity organizations
</li>
<li>students, professors or technical staffs working at an educational institute</li>
<li>activists, dissidents or journalists</li>
<li>charity organizations</li>
</ul>
<p>
Please send us an email at <a href="mailto:support@simplelogin.zendesk.com" target="_blank">support@simplelogin.zendesk.com</a> for more info.
@ -677,9 +647,7 @@
aria-labelledby="pricing-faq-question-refund"
data-parent="#pricing-faq">
<div class="card-body">
<p>
No we don't have a refund policy because SimpleLogin has a trial period where you can try all premium features.
</p>
<p>No we don't have a refund policy because SimpleLogin has a trial period where you can try all premium features.</p>
</div>
</div>
</div>

View File

@ -102,9 +102,7 @@
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ '?slref=' + referral.code }}"
style="overflow-wrap: break-word">
?slref={{ referral.code }}
</em>
style="overflow-wrap: break-word">?slref={{ referral.code }}</em>
to any link on SimpleLogin website.
</div>
<div>

View File

@ -70,9 +70,7 @@
To: {{ alias.email }}
<a href='{{ url_for("dashboard.index", highlight_alias_id=alias.id) }}'
class="text-danger small-text"
style="text-decoration: underline">
Disable Alias
</a>
style="text-decoration: underline">Disable Alias</a>
</span>
{% endif %}
{% if refused_email.deleted %}

View File

@ -35,7 +35,7 @@
<div>
{% if paddle_sub.cancelled %}(Cancelled){% endif %}
{{ paddle_sub.plan_name() }} plan subscribed via Paddle.
<a href="{{ url_for('dashboard.billing') }}">Manage Subscription ➡</a>
<a href="{{ url_for("dashboard.billing") }}">Manage Subscription ➡</a>
</div>
{% endif %}
{% if manual_sub and manual_sub.is_active() %}
@ -48,7 +48,7 @@
<br />
To gain additional features and support SimpleLogin you can upgrade to a Premium plan.
<br />
<a href="{{ url_for('dashboard.pricing') }}"
<a href="{{ url_for("dashboard.pricing") }}"
class="btn btn-sm btn-outline-primary">Upgrade</a>
{% endif %}
</div>
@ -61,7 +61,7 @@
<div class="alert alert-info">
If you want to subscribe via the Web instead, please make sure to cancel your subscription
on Apple first.
<a href="{{ url_for('dashboard.pricing') }}">
<a href="{{ url_for("dashboard.pricing") }}">
Upgrade <i class="fa fa-arrow-right" aria-hidden="true"></i>
</a>
</div>
@ -72,7 +72,7 @@
<div>
Yearly plan subscribed with cryptocurrency which expires on
{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}.
<a href="{{ url_for('dashboard.coinbase_checkout_route') }}"
<a href="{{ url_for("dashboard.coinbase_checkout_route") }}"
target="_blank"
rel="noopener noreferrer">
Extend Subscription <i class="fe fe-external-link"></i>
@ -97,11 +97,7 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="notification-preference">
<div class="form-check">
<input type="checkbox"
id="notification"
name="notification"
{% if current_user.notification %} checked{% endif %}
class="form-check-input">
<input type="checkbox" id="notification" name="notification" {% if current_user.notification %}checked{% endif %} class="form-check-input">
<label for="notification">I want to be emailed when new features are released.</label>
</div>
<button type="submit" class="btn btn-outline-primary">Submit</button>
@ -189,13 +185,11 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-alias-generator">
<select class="form-control mr-sm-2" name="alias-generator-scheme">
<option value="{{ AliasGeneratorEnum.word.value }}"
{% if current_user.alias_generator == AliasGeneratorEnum.word.value %} selected{% endif %}>
<option value="{{ AliasGeneratorEnum.word.value }}" {% if current_user.alias_generator == AliasGeneratorEnum.word.value %}selected{% endif %}>
Based on
Random {{ AliasGeneratorEnum.word.name.capitalize() }}
</option>
<option value="{{ AliasGeneratorEnum.uuid.value }}"
{% if current_user.alias_generator == AliasGeneratorEnum.uuid.value %} selected{% endif %}>
<option value="{{ AliasGeneratorEnum.uuid.value }}" {% if current_user.alias_generator == AliasGeneratorEnum.uuid.value %}selected{% endif %}>
Based
on {{ AliasGeneratorEnum.uuid.name.upper() }}
</option>
@ -209,14 +203,11 @@
name="form-name"
value="change-random-alias-default-domain">
<select class="form-control mr-sm-2" name="random-alias-default-domain">
<option value="">
Not Selected
</option>
<option value="">Not Selected</option>
{% for is_public, domain in current_user.available_domains_for_random_alias() %}
<option value="{{ domain }}" {% if current_user.default_alias_custom_domain_id or current_user.default_alias_public_domain_id %}
{% if current_user.default_random_alias_domain() == domain %}
selected {% endif %} {% endif %}>
{% if current_user.default_random_alias_domain() == domain %}selected{% endif %} {% endif %}>
{{ domain }} (
{% if is_public %}
@ -235,14 +226,8 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="random-alias-suffix">
<select class="form-control mr-sm-2" name="random-alias-suffix-generator">
<option value="0"
{% if current_user.random_alias_suffix==0 %} selected{% endif %}>
Random word from our dictionary
</option>
<option value="1"
{% if current_user.random_alias_suffix==1 %} selected{% endif %}>
Random combination of {{ ALIAS_RAND_SUFFIX_LENGTH }} letter and digits
</option>
<option value="0" {% if current_user.random_alias_suffix==0 %}selected{% endif %}>Random word from our dictionary</option>
<option value="1" {% if current_user.random_alias_suffix==1 %}selected{% endif %}>Random combination of {{ ALIAS_RAND_SUFFIX_LENGTH }} letter and digits</option>
</select>
<button class="btn btn-outline-primary">Update</button>
</form>
@ -258,7 +243,7 @@
<div class="alert alert-info" role="alert">
This feature is only available on Premium plan.
<a href="{{ url_for('dashboard.pricing') }}"
<a href="{{ url_for("dashboard.pricing") }}"
target="_blank"
rel="noopener noreferrer">
Upgrade<i class="fe fe-external-link"></i>
@ -273,11 +258,7 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="enable_data_breach_check">
<div class="form-check">
<input type="checkbox"
id="enable_data_breach_check"
name="enable_data_breach_check"
{% if current_user.enable_data_breach_check %} checked{% endif %}
class="form-check-input">
<input type="checkbox" id="enable_data_breach_check" name="enable_data_breach_check" {% if current_user.enable_data_breach_check %}checked{% endif %} class="form-check-input">
<label for="enable_data_breach_check">Enable data breach monitoring</label>
</div>
<button type="submit" class="btn btn-outline-primary">Update</button>
@ -300,30 +281,13 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-sender-format">
<select class="form-control mr-sm-2" name="sender-format">
<option value="{{ SenderFormatEnum.AT.value }}"
{% if current_user.sender_format == SenderFormatEnum.AT.value %} selected{% endif %}>
John Wick - john at wick.com
</option>
<option value="{{ SenderFormatEnum.A.value }}"
{% if current_user.sender_format == SenderFormatEnum.A.value %} selected{% endif %}>
John Wick - john(a)wick.com
</option>
<option value="{{ SenderFormatEnum.NAME_ONLY.value }}"
{% if current_user.sender_format == SenderFormatEnum.NAME_ONLY.value %} selected{% endif %}>
John Wick
</option>
<option value="{{ SenderFormatEnum.AT_ONLY.value }}"
{% if current_user.sender_format == SenderFormatEnum.AT_ONLY.value %} selected{% endif %}>
john at wick.com
</option>
<option value="{{ SenderFormatEnum.NO_NAME.value }}"
{% if current_user.sender_format == SenderFormatEnum.NO_NAME.value %} selected{% endif %}>
No Name (i.e. only reverse-alias)
</option>
<option value="{{ SenderFormatEnum.AT.value }}" {% if current_user.sender_format == SenderFormatEnum.AT.value %}selected{% endif %}>John Wick - john at wick.com</option>
<option value="{{ SenderFormatEnum.A.value }}" {% if current_user.sender_format == SenderFormatEnum.A.value %}selected{% endif %}>John Wick - john(a)wick.com</option>
<option value="{{ SenderFormatEnum.NAME_ONLY.value }}" {% if current_user.sender_format == SenderFormatEnum.NAME_ONLY.value %}selected{% endif %}>John Wick</option>
<option value="{{ SenderFormatEnum.AT_ONLY.value }}" {% if current_user.sender_format == SenderFormatEnum.AT_ONLY.value %}selected{% endif %}>john at wick.com</option>
<option value="{{ SenderFormatEnum.NO_NAME.value }}" {% if current_user.sender_format == SenderFormatEnum.NO_NAME.value %}selected{% endif %}>No Name (i.e. only reverse-alias)</option>
</select>
<button class="btn btn-outline-primary mt-3">
Update
</button>
<button class="btn btn-outline-primary mt-3">Update</button>
</form>
</div>
</div>
@ -333,9 +297,7 @@
<div class="card-body">
<div class="card-title">
Reverse Alias Replacement
<div class="badge badge-warning">
Experimental
</div>
<div class="badge badge-warning">Experimental</div>
</div>
<div class="mb-3">
When replying to a forwarded email, the <b>reverse-alias</b> can be automatically included
@ -347,18 +309,10 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="replace-ra">
<div class="form-check">
<input type="checkbox"
id="replace-ra"
name="replace-ra"
{% if current_user.replace_reverse_alias %} checked{% endif %}
class="form-check-input">
<label for="replace-ra">
Enable replacing reverse alias
</label>
<input type="checkbox" id="replace-ra" name="replace-ra" {% if current_user.replace_reverse_alias %}checked{% endif %} class="form-check-input">
<label for="replace-ra">Enable replacing reverse alias</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
<button type="submit" class="btn btn-outline-primary">Update</button>
</form>
</div>
</div>
@ -366,9 +320,7 @@
<!-- Sender included in reverse-alias -->
<div class="card" id="sender-in-ra">
<div class="card-body">
<div class="card-title">
Include sender address in reverse-alias
</div>
<div class="card-title">Include sender address in reverse-alias</div>
<div class="mb-3">
If this option is enabled, new reverse-alias will include the sender address (e.g. <span class="italic">sender_domain_com_gibberish@simplelogin.co</span>)
so you can quickly know the sender.
@ -385,13 +337,9 @@
{% if current_user.include_sender_in_reverse_alias is none or current_user.include_sender_in_reverse_alias %}
checked {% endif %} class="form-check-input">
<label for="include-sender-ra">
Include sender address in reverse-alias
</label>
<label for="include-sender-ra">Include sender address in reverse-alias</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
<button type="submit" class="btn btn-outline-primary">Update</button>
</form>
</div>
</div>
@ -399,9 +347,7 @@
<!-- Always expand alias info -->
<div class="card" id="expand-alias-info-section">
<div class="card-body">
<div class="card-title">
Always expand alias info
</div>
<div class="card-title">Always expand alias info</div>
<div class="mb-3">
By default, additional alias info is shown after clicking on the "More" button.
<br />
@ -412,18 +358,10 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="expand-alias-info">
<div class="form-check">
<input type="checkbox"
id="expand-alias-info"
name="enable"
{% if current_user.expand_alias_info %} checked{% endif %}
class="form-check-input">
<label for="expand-alias-info">
Automatically expand alias info
</label>
<input type="checkbox" id="expand-alias-info" name="enable" {% if current_user.expand_alias_info %}checked{% endif %} class="form-check-input">
<label for="expand-alias-info">Automatically expand alias info</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
<button type="submit" class="btn btn-outline-primary">Update</button>
</form>
</div>
</div>
@ -431,9 +369,7 @@
<!-- Include website address in alias -->
<div class="card" id="include_website_in_one_click_alias">
<div class="card-body">
<div class="card-title">
Include website address in one-click alias creation on browser extension
</div>
<div class="card-title">Include website address in one-click alias creation on browser extension</div>
<div class="mb-3">
If enabled, the website name will be used as alias prefix
when you create an alias via SimpleLogin browser extension via the email input field
@ -448,18 +384,10 @@
name="form-name"
value="include_website_in_one_click_alias">
<div class="form-check">
<input type="checkbox"
id="include-website-in-alias"
name="enable"
{% if current_user.include_website_in_one_click_alias %} checked{% endif %}
class="form-check-input">
<label for="include-website-in-alias">
Include website address in alias
</label>
<input type="checkbox" id="include-website-in-alias" name="enable" {% if current_user.include_website_in_one_click_alias %}checked{% endif %} class="form-check-input">
<label for="include-website-in-alias">Include website address in alias</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
<button type="submit" class="btn btn-outline-primary">Update</button>
</form>
</div>
</div>
@ -490,14 +418,10 @@
<!-- One-click subscribe -->
<div class="card" id="one-click-unsubscribe-section">
<div class="card-body">
<div class="card-title">
One-click unsubscribe
</div>
<div class="card-title">One-click unsubscribe</div>
<div class="mb-3">
On email clients that support the
<a href="https://simplelogin.io/docs/getting-started/one-click-unsubscribe/">
One-click unsubscribe
</a>
<a href="https://simplelogin.io/docs/getting-started/one-click-unsubscribe/">One-click unsubscribe</a>
button, clicking on it will allow you to do one of these actions:
<ul>
<li>
@ -519,30 +443,20 @@
<input type="hidden" name="form-name" value="one-click-unsubscribe">
<select class="form-control mr-sm-2" name="unsubscribe-behaviour">
<option value="{{ UnsubscribeBehaviourEnum.PreserveOriginal.name }}" {% if current_user.unsub_behaviour.value == UnsubscribeBehaviourEnum.PreserveOriginal.value %}
selected="selected" {% endif %}>
Preserve the original unsubscribe policy
</option>
selected="selected" {% endif %}>Preserve the original unsubscribe policy</option>
<option value="{{ UnsubscribeBehaviourEnum.DisableAlias.name }}" {% if current_user.unsub_behaviour.value == UnsubscribeBehaviourEnum.DisableAlias.value %}
selected="selected" {% endif %}>
Disable the alias that received the email
</option>
selected="selected" {% endif %}>Disable the alias that received the email</option>
<option value="{{ UnsubscribeBehaviourEnum.BlockContact.name }}" {% if current_user.unsub_behaviour.value == UnsubscribeBehaviourEnum.BlockContact.value %}
selected="selected" {% endif %}>
Block the sender that sent the original email
</option>
selected="selected" {% endif %}>Block the sender that sent the original email</option>
</select>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
<button type="submit" class="btn btn-outline-primary">Update</button>
</form>
</div>
</div>
<!-- END One-click subscribe -->
<div class="card">
<div class="card-body">
<div class="card-title">
Quarantine & Bounces
</div>
<div class="card-title">Quarantine & Bounces</div>
<div class="mb-3">
When an email is refused (or bounced) by your mailbox provider or flagged by
<a href="https://simplelogin.io/docs/getting-started/anti-phishing/">SimpleLogin anti-phishing program</a>,
@ -552,17 +466,13 @@
<br />
This is an exceptional case where SimpleLogin temporarily stores the email.
</div>
<a href="{{ url_for('dashboard.refused_email_route') }}"
class="btn btn-outline-primary">
See quarantine & bounce emails
</a>
<a href="{{ url_for("dashboard.refused_email_route") }}"
class="btn btn-outline-primary">See quarantine & bounce emails</a>
</div>
</div>
<div class="card" id="blocked-behaviour">
<div class="card-body">
<div class="card-title">
Disabled alias/Blocked contact
</div>
<div class="card-title">Disabled alias/Blocked contact</div>
<div class="mb-3">
When an email is sent to a <b>disabled</b> alias or sent from a <b>blocked</b> contact, you can decide what
response the sender should see.
@ -578,26 +488,20 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-blocked-behaviour">
<select class="form-control mr-sm-2" name="blocked-behaviour">
<option value="{{ BlockBehaviourEnum.return_2xx.value }}"
{% if current_user.block_behaviour.value == BlockBehaviourEnum.return_2xx.value %} selected="selected"{% endif %}>
<option value="{{ BlockBehaviourEnum.return_2xx.value }}" {% if current_user.block_behaviour.value == BlockBehaviourEnum.return_2xx.value %}selected="selected"{% endif %}>
Ignore (the sender will see the email as delivered, but you won't receive anything).
</option>
<option value="{{ BlockBehaviourEnum.return_5xx.value }}"
{% if current_user.block_behaviour.value == BlockBehaviourEnum.return_5xx.value %} selected="selected"{% endif %}>
<option value="{{ BlockBehaviourEnum.return_5xx.value }}" {% if current_user.block_behaviour.value == BlockBehaviourEnum.return_5xx.value %}selected="selected"{% endif %}>
Reject (the sender will be told that your alias does not exist).
</option>
</select>
<button class="btn btn-outline-primary">
Update
</button>
<button class="btn btn-outline-primary">Update</button>
</form>
</div>
</div>
<div class="card" id="sender-header">
<div class="card-body">
<div class="card-title">
Include original sender in email headers
</div>
<div class="card-title">Include original sender in email headers</div>
<div class="mb-3">
SimpleLogin forwards emails to your mailbox from the <b>reverse-alias</b> and not from the <b>original</b>
sender address.
@ -612,34 +516,24 @@
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="sender-header">
<div class="form-check">
<input type="checkbox"
id="include-sender-header"
name="enable"
{% if current_user.include_header_email_header %} checked{% endif %}
class="form-check-input">
<label for="include-sender-header">
Include sender address in email headers
</label>
<input type="checkbox" id="include-sender-header" name="enable" {% if current_user.include_header_email_header %}checked{% endif %} class="form-check-input">
<label for="include-sender-header">Include sender address in email headers</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
<button type="submit" class="btn btn-outline-primary">Update</button>
</form>
</div>
</div>
<!-- Alias import/export -->
<div class="card">
<div class="card-body">
<div class="card-title">
Alias import/export
</div>
<div class="card-title">Alias import/export</div>
<div class="mb-3">
You can import your aliases created on other platforms into SimpleLogin.
You can also export your aliases to a readable csv format for a future batch import.
</div>
<a href="{{ url_for('dashboard.batch_import_route') }}"
<a href="{{ url_for("dashboard.batch_import_route") }}"
class="btn btn-outline-primary">Batch Import</a>
<a href="{{ url_for('dashboard.alias_export_route') }}"
<a href="{{ url_for("dashboard.alias_export_route") }}"
class="btn btn-outline-secondary">Export Aliases</a>
</div>
</div>

View File

@ -23,16 +23,14 @@
<div class="alert alert-danger" role="alert">
This feature is only available on Premium plan.
<a href="{{ url_for('dashboard.pricing') }}"
<a href="{{ url_for("dashboard.pricing") }}"
target="_blank"
rel="noopener noreferrer">
Upgrade<i class="fe fe-external-link"></i>
</a>
</div>
{% endif %}
<div class="alert alert-primary collapse {% if not subdomains %} show{% endif %}"
id="howtouse"
role="alert">
<div class="alert alert-primary collapse {% if not subdomains %}show{% endif %}" id="howtouse" role="alert">
You can use subdomain to quickly create email aliases without opening SimpleLogin app.
<br />
Handy when you need to quickly give out an email address, for example on a phone call, in a meeting or just
@ -65,8 +63,7 @@
</div>
{% endfor %}
</div>
<div class="row {% if current_user.subdomain_quota <= 0 %} disabled-content{% endif %}"
id="new-subdomain">
<div class="row {% if current_user.subdomain_quota <= 0 %}disabled-content{% endif %}" id="new-subdomain">
<div class="col">
<div class="card">
<div class="card-body">
@ -88,12 +85,7 @@
<div class="form-group">
<label>Root domain</label>
<select name="domain" v-model="domain" class="form-control">
{% for sl_domain in sl_domains %}
<option value="{{ sl_domain.domain }}">
{{ sl_domain.domain }}
</option>
{% endfor %}
{% for sl_domain in sl_domains %}<option value="{{ sl_domain.domain }}">{{ sl_domain.domain }}</option>{% endfor %}
</select>
</div>
<button class="btn btn-primary">Create</button>

View File

@ -1,6 +1,6 @@
{% extends "default.html" %}
{% set active_page = 'dashboard' %}
{% set active_page = "dashboard" %}
{% block title %}Support{% endblock %}
{% block default_content %}
@ -10,7 +10,7 @@
<div class="card-title mb-3">Report a problem</div>
<div class="alert alert-info">
If an email cannot be delivered to your mailbox, please check
<a href="{{ url_for('dashboard.notifications_route') }}">
<a href="{{ url_for("dashboard.notifications_route") }}">
your
notifications
</a>
@ -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>
@ -28,7 +28,12 @@
<form id="supportZendeskForm" method="post" enctype="multipart/form-data">
<div class="mt-4 mb-5">
<label for="issueDescription" class="form-label font-weight-bold">What happened?</label>
<textarea class="form-control" required name="ticket_content" id="issueDescription" rows="3" placeholder="Please provide as much information as possible. For example which alias(es), mailbox(es) are affected, if this is a persistent issue...">{{- ticket_content or '' -}}</textarea>
<textarea class="form-control"
required
name="ticket_content"
id="issueDescription"
rows="3"
placeholder="Please provide as much information as possible. For example which alias(es), mailbox(es) are affected, if this is a persistent issue...">{{- ticket_content or '' -}}</textarea>
</div>
<div class="mt-5 font-weight-bold">Attach files to support request</div>
<div class="text-muted">Only images, text and emails are accepted</div>
@ -57,9 +62,7 @@
<button class="btn btn-outline-primary"
type="button"
@click="generateRandomAlias"
id="button-addon2">
Generate a random alias
</button>
id="button-addon2">Generate a random alias</button>
</div>
</div>
<div class="mt-5">

View File

@ -7,14 +7,9 @@
<div class="form-group">
<label class="form-label">OpenID Connect Discovery Document</label>
<div class="input-group mt-2">
<input type="text"
disabled
value="{{ URL + "/.well-known/openid-configuration" }}"
class="form-control">
<input type="text" disabled value="{{ URL + "/.well-known/openid-configuration" }}" class="form-control">
<span class="input-group-append">
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}"
class="clipboard btn btn-primary"
type="button">
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}" class="clipboard btn btn-primary" type="button">
<i class="fe fe-clipboard"></i>
</button>
</span>
@ -23,14 +18,9 @@
<div class="form-group">
<label class="form-label">Authorization endpoint</label>
<div class="input-group mt-2">
<input type="text"
disabled
value="{{ URL + "/oauth2/authorize" }}"
class="form-control">
<input type="text" disabled value="{{ URL + "/oauth2/authorize" }}" class="form-control">
<span class="input-group-append">
<button data-clipboard-text="{{ URL + "/oauth2/authorize" }}"
class="clipboard btn btn-primary"
type="button">
<button data-clipboard-text="{{ URL + "/oauth2/authorize" }}" class="clipboard btn btn-primary" type="button">
<i class="fe fe-clipboard"></i>
</button>
</span>
@ -39,14 +29,9 @@
<div class="form-group">
<label class="form-label">Token endpoint</label>
<div class="input-group mt-2">
<input type="text"
disabled
value="{{ URL + "/oauth2/token" }}"
class="form-control">
<input type="text" disabled value="{{ URL + "/oauth2/token" }}" class="form-control">
<span class="input-group-append">
<button data-clipboard-text="{{ URL + "/oauth2/token" }}"
class="clipboard btn btn-primary"
type="button">
<button data-clipboard-text="{{ URL + "/oauth2/token" }}" class="clipboard btn btn-primary" type="button">
<i class="fe fe-clipboard"></i>
</button>
</span>
@ -55,14 +40,9 @@
<div class="form-group">
<label class="form-label">UserInfo endpoint</label>
<div class="input-group mt-2">
<input type="text"
disabled
value="{{ URL + "/oauth2/userinfo" }}"
class="form-control">
<input type="text" disabled value="{{ URL + "/oauth2/userinfo" }}" class="form-control">
<span class="input-group-append">
<button data-clipboard-text="{{ URL + "/oauth2/userinfo" }}"
class="clipboard btn btn-primary"
type="button">
<button data-clipboard-text="{{ URL + "/oauth2/userinfo" }}" class="clipboard btn btn-primary" type="button">
<i class="fe fe-clipboard"></i>
</button>
</span>

View File

@ -6,7 +6,7 @@
<h1 class="h2">Referral</h1>
<div>
If you are in the
<a href="{{ url_for('dashboard.referral_route') }}">referral</a>
<a href="{{ url_for("dashboard.referral_route") }}">referral</a>
program, you can attach a
referral to this website.
Any SimpleLogin sign up thanks to the SIWSL on your website will be counted towards this referral.
@ -17,17 +17,9 @@
<select class="form-control" name="referral-id" id="client-select">
{% for referral in current_user.referrals %}
<option value="{{ referral.id }}"
{% if client.referral_id == referral.id %} selected{% endif %}>
{{ referral.name }}
</option>
<option value="{{ referral.id }}" {% if client.referral_id == referral.id %}selected{% endif %}>{{ referral.name }}</option>
{% endfor %}
{% if client.referral_id is none %}
<option value="" selected>
No referral selected
</option>
{% endif %}
{% if client.referral_id is none %}<option value="" selected>No referral selected</option>{% endif %}
</select>
</div>
<input type="submit" class="btn btn-primary" value="Update">

View File

@ -18,9 +18,7 @@
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
<div class="alert alert-primary collapse {% if not clients %} show{% endif %}"
id="howtouse"
role="alert">
<div class="alert alert-primary collapse {% if not clients %}show{% endif %}" id="howtouse" role="alert">
If you want to integrate SIWSL into your website,
this page is for you.
<br />
@ -28,14 +26,9 @@
If you are using a CMS or any system that supports a OpenID Connect plugin, you can just point
it to SimpleLogin OpenID Configuration endpoint 👇
<div class="input-group mt-2">
<input type="text"
disabled
value="{{ URL + "/.well-known/openid-configuration" }}"
class="form-control">
<input type="text" disabled value="{{ URL + "/.well-known/openid-configuration" }}" class="form-control">
<span class="input-group-append">
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}"
class="clipboard btn btn-primary"
type="button">
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}" class="clipboard btn btn-primary" type="button">
<i class="fe fe-clipboard"></i>
</button>
</span>
@ -46,7 +39,7 @@
<div class="row">
<div class="col">
<div class="btn-group" role="group" aria-label="Basic example">
<a href="{{ url_for('developer.new_client') }}" class="btn btn-primary">New website</a>
<a href="{{ url_for("developer.new_client") }}" class="btn btn-primary">New website</a>
<a href="https://simplelogin.io/docs/siwsl/app/"
target="_blank"
rel="noopener noreferrer"

View File

@ -2,14 +2,14 @@
<p style="font-size: 16px;
line-height: 1.625;
color: #51545E;
margin: .4em 0 1.1875em;">{{ text }}</p>
margin: .4em 0 1.1875em">{{ text }}</p>
{% endmacro %}
<!-- To be used instead of render_text, much better! -->
{% macro text() %}
<p style="font-size: 16px;
line-height: 1.625;
color: #51545E;
margin: .4em 0 1.1875em;">{{ caller() }}</p>
margin: .4em 0 1.1875em">{{ caller() }}</p>
{% endmacro %}
{% macro render_button(button_text, link) %}
<!-- Action -->
@ -25,12 +25,12 @@
-premailer-cellspacing: 0;
text-align: center;
margin: 30px auto;
padding: 0;">
padding: 0">
<tr>
<td align="center"
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;">
font-size: 16px">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%"
@ -42,7 +42,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<td align="center"
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;">
font-size: 16px">
<a href="{{ link }}"
class="f-fallback button"
target="_blank"
@ -57,9 +57,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;">
{{ button_text }}
</a>
box-sizing: border-box">{{ button_text }}</a>
</td>
</tr>
</table>
@ -75,25 +73,23 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
padding-top: 25px;
border-top-width: 1px;
border-top-color: #EAEAEC;
border-top-style: solid;">
border-top-style: solid">
<tr>
<td style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;">
font-size: 16px">
<p class="f-fallback sub"
style="font-size: 13px;
line-height: 1.625;
color: #51545E;
margin: .4em 0 1.1875em;">
margin: .4em 0 1.1875em">
If youre having trouble with the button above, copy and paste the URL below into your web browser.
</p>
<p class="f-fallback sub"
style="font-size: 13px;
line-height: 1.625;
color: #51545E;
margin: .4em 0 1.1875em;">
{{ link }}
</p>
margin: .4em 0 1.1875em">{{ link }}</p>
</td>
</tr>
</table>
@ -104,14 +100,14 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin: 0 0 21px;">
style="margin: 0 0 21px">
<tr>
<td class="attributes_content"
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
background-color: #F4F4F7;
padding: 16px;"
padding: 16px"
bgcolor="#F4F4F7">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
{% for part in parts %}
@ -121,7 +117,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
padding: 0;">
padding: 0">
<div class="f-fallback">
{{ part }}
<br />

View File

@ -1,5 +1,5 @@
{% from "_emailhelpers.html" import render_text, text, render_button, raw_url, grey_section, section %}
<!doctype html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
@ -504,7 +504,7 @@
padding: 0;
width: 100%;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;">
-webkit-text-size-adjust: 100%">
<!--[if !gte mso 9]><!----><span class="mcnPreviewText"
style="display:none;
font-size:0px;
@ -514,7 +514,7 @@
opacity:0;
overflow:hidden;
visibility:hidden;
mso-hide:all;"></span><!--<![endif]-->
mso-hide:all"></span><!--<![endif]-->
<center>
<table align="center"
border="0"
@ -531,7 +531,7 @@
height: 100%;
margin: 0;
padding: 0;
width: 100%;">
width: 100%">
<tr>
<td align="center"
valign="top"
@ -542,7 +542,7 @@
height: 100%;
margin: 0;
padding: 8px;
width: 100%;">
width: 100%">
<!-- BEGIN TEMPLATE // -->
<!--[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;"><tr><td align="center" valign="top" width="600" style="width:600px;">
@ -557,13 +557,13 @@
mso-table-rspace: 0pt;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
max-width: 600px !important;">
max-width: 600px !important">
<tr>
<td valign="top"
id="templateHeader"
style="mso-line-height-rule: exactly;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;">
-webkit-text-size-adjust: 100%">
<table border="0"
cellpadding="0"
cellspacing="0"
@ -574,14 +574,14 @@
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;">
-webkit-text-size-adjust: 100%">
<tbody class="mcnImageBlockOuter">
<tr>
<td valign="top"
style="padding: 16px;
mso-line-height-rule: exactly;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;"
-webkit-text-size-adjust: 100%"
class="mcnImageBlockInner">
<table align="left"
width="100%"
@ -594,7 +594,7 @@
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;">
-webkit-text-size-adjust: 100%">
<tbody>
<tr>
<td class="mcnImageContent"
@ -603,13 +603,22 @@
text-align: center;
mso-line-height-rule: exactly;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;">
-webkit-text-size-adjust: 100%">
<a href="https://proton.me/" target="_blank" style="">
<img align="center"
alt="Proton"
src="{{ URL }}/static/logo-proton.png"
width="190"
style="width:35.4477%; max-width: 380px; padding-bottom: 0; display: inline !important; vertical-align: bottom; border: 0; height: auto; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; ">
style="width:35.4477%;
max-width: 380px;
padding-bottom: 0;
display: inline !important;
vertical-align: bottom;
border: 0;
height: auto;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic">
</a>
</td>
</tr>
@ -624,7 +633,9 @@
<tr>
<td valign="top"
id="templateBody"
style="mso-line-height-rule: exactly; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; ">
style="mso-line-height-rule: exactly;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%">
{% block greeting %}{% endblock %}
{% block content %}{% endblock %}
<!-- Sub copy -->

View File

@ -449,7 +449,7 @@
font-family: Helvetica, Arial, sans-serif;
background-color: #F2F4F6;
color: #51545E;
margin: 0;"
margin: 0"
bgcolor="#F2F4F6">
<span class="preheader"
style="display: none !important;
@ -460,7 +460,7 @@
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;">{{ pre_header }}</span>
overflow: hidden">{{ pre_header }}</span>
<table class="email-wrapper"
width="100%"
cellpadding="0"
@ -472,13 +472,13 @@
-premailer-cellspacing: 0;
background-color: #F2F4F6;
margin: 0;
padding: 0;"
padding: 0"
bgcolor="#F2F4F6">
<tr>
<td align="center"
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;">
font-size: 16px">
<table class="email-content"
width="100%"
cellpadding="0"
@ -489,14 +489,14 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
margin: 0;
padding: 0;">
padding: 0">
<tr>
<td class="email-masthead"
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
text-align: center;
padding: 25px 0;"
padding: 25px 0"
align="center">
<a href="{{ LANDING_PAGE_URL }}"
class="f-fallback email-masthead_name"
@ -504,7 +504,7 @@
font-size: 16px;
font-weight: bold;
text-decoration: none;
text-shadow: 0 1px 0 white;">
text-shadow: 0 1px 0 white">
{% block logo %}<img src="{{ URL }}/static/logo.png" style="width: 150px; margin: auto">{% endblock %}
</a>
</td>
@ -523,7 +523,7 @@
width: 100%;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;">
-premailer-cellspacing: 0">
<table class="email-body_inner"
align="center"
width="750"
@ -536,7 +536,7 @@
-premailer-cellspacing: 0;
background-color: #FFFFFF;
margin: 0 auto;
padding: 0;"
padding: 0"
bgcolor="#FFFFFF">
<!-- Body content -->
<tr>
@ -544,7 +544,7 @@
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
padding: 30px;">
padding: 30px">
<div class="f-fallback">
{% block greeting %}{% endblock %}
{% block content %}{% endblock %}
@ -559,7 +559,7 @@
<tr>
<td style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;">
font-size: 16px">
<table class="email-footer"
align="center"
width="750"
@ -572,20 +572,20 @@
-premailer-cellspacing: 0;
text-align: center;
margin: 0 auto;
padding: 0;">
padding: 0">
<tr>
<td class="content-cell"
align="center"
style="word-break: break-word;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
padding: 30px;">
padding: 30px">
<p class="f-fallback sub align-center"
style="font-size: 13px;
line-height: 1.625;
text-align: center;
color: #A8AAAF;
margin: .4em 0 1.1875em;"
margin: .4em 0 1.1875em"
align="center">
© {{ YEAR }} SimpleLogin - a Proton product. All rights reserved.
<br />
@ -597,7 +597,7 @@
style="font-size: 13px;
line-height: 1.625;
text-align: center;
margin: .4em 0 1.1875em;">
margin: .4em 0 1.1875em">
<a href="{{ unsubscribe_oneclick }}">Unsubscribe from our newsletter</a>
</p>
{% endif %}
@ -606,7 +606,7 @@
line-height: 1.625;
text-align: center;
color: #A8AAAF;
margin: .4em 0 1.1875em;"
margin: .4em 0 1.1875em"
align="center">
<a href="https://app.simplelogin.io/dashboard/support">Do you have a question?</a>
</p>

View File

@ -20,7 +20,7 @@
{{ render_text('Thanks,
<br />
SimpleLogin Team.') }}
{{ render_text('<strong>P.S.</strong> Need immediate help getting started? Just reply to this email, the SimpleLogin support team is always ready to help!.') }}
{{ render_text("<strong>P.S.</strong> Need immediate help getting started? Just reply to this email, the SimpleLogin support team is always ready to help!.") }}
{% endblock %}
{% block footer %}

View File

@ -5,8 +5,8 @@
{{ render_text("Hi") }}
{{ render_text("If you happen to use Gmail, Yahoo, Outlook, etc, do you know these services can read your emails?") }}
{{ render_text("If you want to keep your emails only readable by you, Pretty Good Privacy (PGP) is maybe the solution.") }}
{{ render_text('Highly recommended, open source and free, PGP is unfortunately not widely supported. However with SimpleLogin most recent PGP support, you can now enable PGP on emails sent to your aliases easily.') }}
{{ render_text('Without PGP the emails sent to an alias are forwarded by SimpleLogin as-is to your mailbox, leaving anyone in-between or your email service able to read your emails:') }}
{{ render_text("Highly recommended, open source and free, PGP is unfortunately not widely supported. However with SimpleLogin most recent PGP support, you can now enable PGP on emails sent to your aliases easily.") }}
{{ render_text("Without PGP the emails sent to an alias are forwarded by SimpleLogin as-is to your mailbox, leaving anyone in-between or your email service able to read your emails:") }}
<img src="https://simplelogin.io/blog/without-pgp.png"
alt="Without PGP"
style="max-width: 100%">
@ -24,7 +24,7 @@
{{ render_text('Thanks,
<br />
SimpleLogin Team.') }}
{{ render_text('<strong>P.S.</strong> Need immediate help getting started? Just reply to this email, the SimpleLogin support team is always ready to help!.') }}
{{ render_text("<strong>P.S.</strong> Need immediate help getting started? Just reply to this email, the SimpleLogin support team is always ready to help!.") }}
{% endblock %}
{% block footer %}

View File

@ -17,7 +17,7 @@
line-height: 160%;
padding-top: 25px;
color: #000000;
font-family: sans-serif;"
font-family: sans-serif"
class="paragraph">
This email is sent to {{ user.email }}.
Unsubscribe on

View File

@ -26,7 +26,7 @@ or
<img src="https://simplelogin.io/images/everywhere.png"
alt="Available Everywhere"
style="max-width: 100%;">
style="max-width: 100%">
{% endblock %}
{% block footer %}

View File

@ -13,7 +13,9 @@ you can add them into SimpleLogin as <b>mailboxes</b>.
<img src="https://simplelogin.io/images/multiple-mailboxes.png"
alt="Multiple Mailboxes"
style="max-width: 100%; margin: auto; border: 1px solid">
style="max-width: 100%;
margin: auto;
border: 1px solid">
{% call text() %}
When creating an alias, you can choose the mailbox(es) that
<b>owns</b> this alias, meaning:

View File

@ -19,14 +19,16 @@ Without PGP, emails are stored <b>in plaintext</b> leaving your email service ab
<img src="https://simplelogin.io/blog/without-pgp.png"
alt="Without PGP"
style="max-width: 100%; margin-bottom: 10px">
style="max-width: 100%;
margin-bottom: 10px">
{% call text() %}
With PGP enabled, SimpleLogin <b>encrypts</b> your emails with your public key before forwarding to your mailbox.
{% endcall %}
<img src="https://simplelogin.io/blog/with-pgp.png"
alt="Without PGP"
style="max-width: 100%; margin-bottom: 20px">
style="max-width: 100%;
margin-bottom: 20px">
{{ render_button("Enable PGP on your mailbox", URL ~ "/dashboard/mailbox/" ~ user.default_mailbox_id) }}
{{ raw_url(URL ~ "/dashboard/mailbox/" ~ user.default_mailbox_id) }}
{% endblock %}

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

@ -6,10 +6,8 @@
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;"
align="left">
Welcome!
</h1>
text-align: left"
align="left">Welcome!</h1>
{% endblock %}
{% block content %}
@ -41,7 +39,9 @@ to create aliases in one click (like in the below gif 👇).
{% call text() %}
<img src="https://simplelogin.io/images/one-click-alias.gif"
style="max-width: 80%; margin: auto; border: 1px solid">
style="max-width: 80%;
margin: auto;
border: 1px solid">
{% endcall %}
{% call text() %}
@ -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

@ -16,7 +16,7 @@ by your mailbox {{ mailbox_email }}.
As security measure, we have disabled the alias.
{% endcall %}
{{ render_text('Please let us know if you have any question.') }}
{{ render_text("Please let us know if you have any question.") }}
{{ render_text('Thanks,
<br />
SimpleLogin Team.') }}

View File

@ -14,7 +14,7 @@ This is usually because your mailbox service thinks the email is <b>spam</b>.
{% endcall %}
{{ render_button("View the bounced email", refused_email_url) }}
{{ render_text('The email is automatically deleted in 7 days.') }}
{{ render_text("The email is automatically deleted in 7 days.") }}
{% call text() %}
Please consider the following options:
<br />

View File

@ -8,7 +8,7 @@
{{ render_text("An email has been sent to the alias <b>" + alias + "</b> that would be created automatically as you own the directory <b>" + directory + "</b>.") }}
{{ render_text("However you have reached the alias limit in your current plan, this creation cannot happen.") }}
{{ render_text('Please upgrade to premium plan in order to use this feature.') }}
{{ render_text("Please upgrade to premium plan in order to use this feature.") }}
{{ render_text('Thanks,
<br />
SimpleLogin Team.') }}

View File

@ -8,7 +8,7 @@
{{ render_text("An email has been sent to the alias <b>" + alias + "</b> that would be created automatically as you own the domain <b>" + domain + "</b>.") }}
{{ render_text("However you have reached the alias limit in your current plan, this creation cannot happen.") }}
{{ render_text('Please upgrade to premium plan in order to use this feature.') }}
{{ render_text("Please upgrade to premium plan in order to use this feature.") }}
{{ render_text('Thanks,
<br />
SimpleLogin Team.') }}

View File

@ -17,7 +17,7 @@
{%- for breach in breaches[:4] %}
<li>
<b>{{ breach.name }}</b>
{% if breach.date %}({{ breach.date.format('YYYY-MM-DD') }}){% endif %}
{% if breach.date %}({{ breach.date.format("YYYY-MM-DD") }}){% endif %}
{{ breach.description }}
</li>
{%- endfor %}

View File

@ -2,10 +2,10 @@
{% block content %}
{{ render_text('This is an automated email from SimpleLogin.') }}
{{ render_text("This is an automated email from SimpleLogin.") }}
{{ render_text("We have recorded an attempt to send an email from your email <b>" + sender + "</b> to <b>" + reply_email + "</b>.") }}
{{ render_text(reply_email + ' is a special email address that only receives emails from its authorized user.') }}
{{ render_text('This user has been also informed of this incident.') }}
{{ render_text("This user has been also informed of this incident.") }}
{{ render_text('If you have any question, you can contact us by replying to this email or consult our website at ' ~ LANDING_PAGE_URL ~ '.') }}
{{ render_text('Regards,
<br />

View File

@ -24,7 +24,7 @@ Your alias {{ alias.email }} is probably in the hands of a spammer now. In this
{% endcall %}
{{ render_button("Disable alias", disable_alias_link) }}
{{ render_text('Please let us know if you have any question by replying to this email.') }}
{{ render_text("Please let us know if you have any question by replying to this email.") }}
{{ render_text('Thanks,
<br />
SimpleLogin Team.') }}

View File

@ -17,7 +17,7 @@ those aliases cannot be automatically created if you have more than {{ MAX_NB_EM
{% endcall %}
{{ render_text("- You cannot add new domain or directory.") }}
{{ render_text('You can upgrade today to continue using all these Premium features (and much more coming).') }}
{{ render_text("You can upgrade today to continue using all these Premium features (and much more coming).") }}
{{ render_button("Upgrade your account", URL ~ "/dashboard/pricing") }}
{{ render_text('Regardless of your choice, we want to say thank you for trying SimpleLogin. We know the product
requires an investment of your time, and we appreciate you giving us a chance.') }}

View File

@ -16,7 +16,7 @@
{{ render_text("- You cannot add new mailbox.") }}
{{ render_text("- You cannot create new reverse aliases.") }}
{{ render_text("- If you enable PGP Encryption, forwarded emails are not encrypted anymore.") }}
{{ render_text('You can upgrade today to continue using all these Premium features (and much more coming).') }}
{{ render_text("You can upgrade today to continue using all these Premium features (and much more coming).") }}
{{ render_button("Upgrade your account", URL ~ "/dashboard/pricing") }}
{{ render_text("If you're not ready to upgrade to a paying account, you have a few other options available to you:") }}
{{ grey_section([

View File

@ -1,7 +1,9 @@
<!-- ========== FOOTER ========== -->
<footer class="gradient-half-primary-v1"
id="footer"
style="background-image: linear-gradient( 150deg ,#2d1582,#19a0ff); background-repeat: repeat-x; list-style: none initial; ">
style="background-image: linear-gradient( 150deg ,#2d1582,#19a0ff);
background-repeat: repeat-x;
list-style: none initial">
<div class="container space-top-2 space-bottom-1 mt-7">
<div class="row justify-content-lg-start mb-7">
<div class="col-sm-7 col-lg-6">
@ -93,7 +95,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>
@ -103,16 +105,12 @@
<li>
<a class="list-group-item text-white footer-item "
rel="noopener noreferrer"
href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">
Chrome Extension
</a>
href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">Chrome Extension</a>
</li>
<li>
<a class="list-group-item text-white footer-item "
rel="noopener noreferrer"
href="https://addons.mozilla.org/firefox/addon/simplelogin/">
Firefox Add-on
</a>
href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox Add-on</a>
</li>
<li>
<a class="list-group-item text-white footer-item "
@ -140,16 +138,12 @@
<li>
<a class="list-group-item text-white footer-item "
rel="noopener noreferrer"
href="https://play.google.com/store/apps/details?id=io.simplelogin.android">
Android (Play Store)
</a>
href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Android (Play Store)</a>
</li>
<li>
<a class="list-group-item text-white footer-item "
rel="noopener noreferrer"
href="https://f-droid.org/en/packages/io.simplelogin.android.fdroid/">
Android (F-Droid)
</a>
href="https://f-droid.org/en/packages/io.simplelogin.android.fdroid/">Android (F-Droid)</a>
</li>
</ul>
</div>

View File

@ -1,7 +1,7 @@
<div class="header">
<div class="container">
<div class="d-flex">
<a class="header-brand" href="{{ url_for('dashboard.index') }}">
<a class="header-brand" href="{{ url_for("dashboard.index") }}">
<picture>
<source media="(max-width: 650px)" srcset="/static/logo-without-text.svg">
<img src="/static/logo.svg"
@ -41,7 +41,7 @@
<div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow collapse"
id="notifications">
<div v-if="loading">Loading ...</div>
<a href="{{ url_for('dashboard.notifications_route') }}"
<a href="{{ url_for("dashboard.notifications_route") }}"
class="mr-5 mb-2 float-right">See all notifications ➡</a>
<div class="dropdown-item d-flex" v-for="notification in notifications">
<div class="flex-grow-1">
@ -50,8 +50,7 @@
style="width: 40em;
word-wrap:break-word;
white-space: normal;
overflow: hidden;">
</div>
overflow: hidden"></div>
<div v-if="notification.title">
<a :href="'/dashboard/notification/' + notification.id">More</a>
</div>
@ -91,7 +90,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
@ -116,7 +115,7 @@
{% if current_user.should_show_upgrade_button() %}
<div class="nav-item">
<a href="{{ url_for('dashboard.pricing') }}"
<a href="{{ url_for("dashboard.pricing") }}"
class="btn btn-sm btn-outline-primary">Upgrade</a>
</div>
{% endif %}
@ -145,14 +144,14 @@
</span>
</a>
<div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow">
<a class="dropdown-item mb-3" href="{{ url_for('dashboard.api_key') }}">
<a class="dropdown-item mb-3" href="{{ url_for("dashboard.api_key") }}">
<i class="dropdown-icon fa fa-key"></i> API Keys
</a>
<a class="dropdown-item mb-3"
href="{{ url_for('dashboard.account_setting') }}">
href="{{ url_for("dashboard.account_setting") }}">
<i class="dropdown-icon fa fa-user"></i> Account settings
</a>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<a class="dropdown-item" href="{{ url_for("auth.logout") }}">
<i class="dropdown-icon fe fe-log-out"></i> Sign out
</a>
</div>

View File

@ -1,6 +1,6 @@
<ul class="nav nav-tabs border-0 flex-column flex-lg-row">
<li class="nav-item">
<a href="{{ url_for('dashboard.index') }}"
<a href="{{ url_for("dashboard.index") }}"
class="nav-link {{ 'active' if active_page == 'dashboard' }}">
<i class="fe fe-home"></i>
Aliases
@ -9,26 +9,26 @@
{% if current_user.subdomain_is_available() %}
<li class="nav-item">
<a href="{{ url_for('dashboard.subdomain_route') }}"
<a href="{{ url_for("dashboard.subdomain_route") }}"
class="nav-link {{ 'active' if active_page == 'subdomain' }}">
<i class="fe fe-server"></i> Subdomains
</a>
</li>
{% endif %}
<li class="nav-item">
<a href="{{ url_for('dashboard.mailbox_route') }}"
<a href="{{ url_for("dashboard.mailbox_route") }}"
class="nav-link {{ 'active' if active_page == 'mailbox' }}">
<i class="fe fe-inbox"></i> Mailboxes
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('dashboard.custom_domain') }}"
<a href="{{ url_for("dashboard.custom_domain") }}"
class="nav-link {{ 'active' if active_page == 'custom_domain' }}">
<i class="fe fe-server"></i> Domains
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('dashboard.directory') }}"
<a href="{{ url_for("dashboard.directory") }}"
class="nav-link {{ 'active' if active_page == 'directory' }}">
<i class="fe fe-folder"></i> Directories
</a>
@ -51,7 +51,7 @@
{% if current_user.should_show_app_page() %}
<li class="nav-item">
<a href="{{ url_for('dashboard.app_route') }}"
<a href="{{ url_for("dashboard.app_route") }}"
class="nav-link {{ 'active' if active_page == 'app' }}">
<i class="fe fe-grid"></i>
Apps
@ -59,7 +59,7 @@
</li>
{% endif %}
<li class="nav-item">
<a href="{{ url_for('dashboard.setting') }}"
<a href="{{ url_for("dashboard.setting") }}"
class="nav-link {{ 'active' if active_page == 'setting' }}">
<i class="fe fe-settings"></i>
Settings
@ -75,12 +75,13 @@
{% if current_user.can_use_phone %}
<li class="nav-item">
<a href="{{ url_for('phone.index') }}"
<a href="{{ url_for("phone.index") }}"
class="nav-link {{ 'active' if active_page == 'phone' }}">
<i class="fe fe-phone"></i>
Phone
<span class="badge badge-warning"
style="line-height: .7em; margin-top: 2px">Beta</span>
style="line-height: .7em;
margin-top: 2px">Beta</span>
</a>
</li>
{% endif %}
@ -107,7 +108,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

@ -33,7 +33,9 @@
<form class="card"
method="post"
data-parsley-validate
style="max-width: 40rem; margin: auto; border-radius: 2%">
style="max-width: 40rem;
margin: auto;
border-radius: 2%">
{% if not client.approved %}
<div class="alert alert-warning" style="border-bottom: 3px solid;">
@ -85,18 +87,9 @@
</div>
<div class="col-md-9">
<select class="form-control" name="suggested-email">
<option selected value="{{ suggested_email }}">
{{ suggested_email }}
</option>
<option value="{{ current_user.email }}">
{{ current_user.email }} (Personal Email)
</option>
{% for email in other_emails %}
<option value="{{ email }}">
{{ email }}
</option>
{% endfor %}
<option selected value="{{ suggested_email }}">{{ suggested_email }}</option>
<option value="{{ current_user.email }}">{{ current_user.email }} (Personal Email)</option>
{% for email in other_emails %}<option value="{{ email }}">{{ email }}</option>{% endfor %}
</select>
{% if current_user.can_create_new_alias() %}
@ -138,15 +131,8 @@
</div>
<div class="col-md-9">
<select class="form-control" name="suggested-name">
<option selected value="{{ suggested_name }}">
{{ suggested_name }}
</option>
{% for name in other_names %}
<option value="{{ name }}">
{{ name }}
</option>
{% endfor %}
<option selected value="{{ suggested_name }}">{{ suggested_name }}</option>
{% for name in other_names %}<option value="{{ name }}">{{ name }}</option>{% endfor %}
</select>
<div class="mt-2">OR</div>
<div class="mt-2">

View File

@ -5,7 +5,7 @@
<div class="flex-fill align-items-center mt-8">
<!-- Image container -->
<div class="mt-4 mb-4 text-center" style="display:block;">
<a href="{{ url_for('dashboard.index') }}">
<a href="{{ url_for("dashboard.index") }}">
<picture>
<source media="(max-width: 650px)" srcset="/static/logo.svg">
<img src="/static/logo.svg" style="width: 12rem" alt="logo">
@ -23,10 +23,8 @@
style="background:black;
color:white;
display:block;
width:50%;"
href="{{ extension_link }}">
Install {{ browser_name }} extension
</a>
width:50%"
href="{{ extension_link }}">Install {{ browser_name }} extension</a>
</div>
<hr style="margin-top: 75px">
<div style="margin-top: 75px">
@ -37,10 +35,8 @@
border-radius: 2px;
border:1px solid black;
display:block;
width:50%;"
href="{{ url_for('dashboard.index') }}">
Continue to our web app
</a>
width:50%"
href="{{ url_for("dashboard.index") }}">Continue to our web app</a>
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@
<div class="flex-fill align-items-center mt-8">
<!-- Image container -->
<div class="mt-4 mb-4 text-center" style="display:block;">
<a href="{{ url_for('dashboard.index') }}">
<a href="{{ url_for("dashboard.index") }}">
<picture>
<source media="(max-width: 650px)" srcset="/static/logo.svg">
<img src="/static/logo.svg" style="width: 12rem" alt="logo">
@ -22,17 +22,17 @@
place-items:center;
width:1024px;
margin:auto;
position:relative;">
position:relative">
<img src="/static/images/onboarding-click-icon.svg"
style="width:400px;
position:absolute;
right:-70px;
top:-90px;"/>
top:-90px" />
<img src="/static/images/onboarding-right-click.svg"
style="position:absolute;
width:440px;
right:-20px;
top:25px;"/>
top:25px" />
<div style="width: 25rem; display: block">
<form method="post">
{{ form.csrf_token }}
@ -40,9 +40,10 @@
{{ render_field_errors(form.email) }}
<button type="submit"
class="p-4 mt-2 text-decoration-none"
style="background:black;color:white; width:10rem; border-radius: 4px">
Send me an email
</button>
style="background:black;
color:white;
width:10rem;
border-radius: 4px">Send me an email</button>
</form>
</div>
</div>
@ -53,7 +54,7 @@
</p>
<p id="extension-version-text"
class="font-weight-light"
style="display:none;"></p>
style="display:none"></p>
</div>
</div>
<script type="text/javascript">

View File

@ -5,7 +5,7 @@
<div class="flex-fill align-items-center mt-8">
<!-- Image container -->
<div class="mt-4 mb-4 text-center" style="display:block;">
<a href="{{ url_for('dashboard.index') }}">
<a href="{{ url_for("dashboard.index") }}">
<picture>
<source media="(max-width: 650px)" srcset="/static/logo.svg">
<img src="/static/logo.svg" style="width: 12rem" alt="logo">
@ -26,18 +26,14 @@
{% else %}
<a class="mx-6 p-4 text-decoration-none"
style="background:black;
color:white;"
href="{{ url_for('auth.register', next=url_for('onboarding.setup_done')) }}">
Create a new account
</a>
color:white"
href="{{ url_for('auth.register', next=url_for('onboarding.setup_done') ) }}">Create a new account</a>
<a class="mx-6 p-4 text-decoration-none"
style="background:white;
color:black;
border-radius: 2px;
border:1px solid black;"
href="{{ url_for('auth.login', next=url_for('onboarding.setup_done')) }}">
I already have an account
</a>
border:1px solid black"
href="{{ url_for('auth.login', next=url_for('onboarding.setup_done') ) }}">I already have an account</a>
{% endif %}
</div>
</div>

View File

@ -5,7 +5,7 @@
<div class="flex-fill align-items-center mt-8">
<!-- Image container -->
<div class="mt-4 mb-4 text-center" style="display:block;">
<a href="{{ url_for('dashboard.index') }}">
<a href="{{ url_for("dashboard.index") }}">
<picture>
<source media="(max-width: 650px)" srcset="/static/logo.svg">
<img src="/static/logo.svg" style="width: 24rem" alt="logo">

View File

@ -12,9 +12,7 @@
{% for reservation in reservations %}
<div>
<a href="{{ url_for('phone.reservation_route', reservation_id=reservation.id ) }}">
{{ reservation.number.number }} ➡
</a>
<a href="{{ url_for('phone.reservation_route', reservation_id=reservation.id) }}">{{ reservation.number.number }} ➡</a>
</div>
{% endfor %}
</div>
@ -66,9 +64,7 @@
<div>
<a href="{{ url_for('phone.reservation_route', reservation_id=reservation.id) }}"
class="mr-3">
{{ reservation.number.number }} ➡
</a>
class="mr-3">{{ reservation.number.number }} ➡</a>
ended {{ reservation.end.humanize() }}
</div>
{% endfor %}

View File

@ -9,7 +9,8 @@
<div class="text-center mb-6">
<a href="{{ LANDING_PAGE_URL }}">
<img src="/static/siwsl.svg"
style="background-color: transparent; height: 80px">
style="background-color: transparent;
height: 80px">
</a>
</div>
{% block single_content %}{% endblock %}

View File

@ -9,7 +9,8 @@
<div class="text-center mb-6">
<a href="{{ LANDING_PAGE_URL }}">
<img src="/static/logo.svg"
style="background-color: transparent; height: 20px">
style="background-color: transparent;
height: 20px">
</a>
</div>
{% block single_content %}{% endblock %}