Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
edef254529 | |||
357f0cca57 | |||
8ce90e27f7 | |||
3ecc8d36f9 | |||
14f4829fab | |||
63ac89e952 | |||
8896f00124 | |||
d313c94f77 |
2
app/.github/workflows/main.yml
vendored
2
app/.github/workflows/main.yml
vendored
@ -109,7 +109,7 @@ jobs:
|
|||||||
GITHUB_ACTIONS_TEST: true
|
GITHUB_ACTIONS_TEST: true
|
||||||
|
|
||||||
- name: Archive code coverage results
|
- name: Archive code coverage results
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: code-coverage-report
|
name: code-coverage-report
|
||||||
path: htmlcov
|
path: htmlcov
|
||||||
|
@ -8,7 +8,7 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/Riverside-Healthcare/djLint
|
- repo: https://github.com/Riverside-Healthcare/djLint
|
||||||
rev: v1.3.0
|
rev: v1.34.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: djlint-jinja
|
- id: djlint-jinja
|
||||||
files: '.*\.html'
|
files: '.*\.html'
|
||||||
@ -21,5 +21,4 @@ repos:
|
|||||||
- id: ruff
|
- id: ruff
|
||||||
args: [ --fix ]
|
args: [ --fix ]
|
||||||
# Run the formatter.
|
# Run the formatter.
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
@ -20,7 +20,7 @@ SimpleLogin backend consists of 2 main components:
|
|||||||
## Install dependencies
|
## Install dependencies
|
||||||
|
|
||||||
The project requires:
|
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.
|
- Node v10 for front-end.
|
||||||
- Postgres 13+
|
- 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.
|
Feel free to use `virtualenv` or similar tools to isolate development environment.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye sync
|
poetry sync
|
||||||
```
|
```
|
||||||
|
|
||||||
On Mac, sometimes you might need to install some other packages via `brew`:
|
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
|
We use pre-commit to run all our linting and static analysis checks. Please run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye run pre-commit install
|
poetry run pre-commit install
|
||||||
```
|
```
|
||||||
|
|
||||||
To install it in your development environment.
|
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
|
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
|
The code is also checked with `flake8`, make sure to run `flake8` before creating the pull request by
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye run flake8
|
poetry run flake8
|
||||||
```
|
```
|
||||||
|
|
||||||
For HTML templates, we use `djlint`. Before creating a pull request, please run
|
For HTML templates, we use `djlint`. Before creating a pull request, please run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye run djlint --check templates
|
poetry run djlint --check templates
|
||||||
```
|
```
|
||||||
|
|
||||||
If some files aren't properly formatted, you can format all files with
|
If some files aren't properly formatted, you can format all files with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rye run djlint --reformat .
|
poetry run djlint --reformat .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test sending email
|
## Test sending email
|
||||||
@ -223,6 +223,31 @@ Now open http://localhost:1080/ (or http://localhost:1080/ for MailHog), you sho
|
|||||||
## Job runner
|
## Job runner
|
||||||
|
|
||||||
Some features require a job handler (such as GDPR data export). To test such feature you need to run the 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
|
```bash
|
||||||
python job_runner.py
|
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
|
||||||
|
|
||||||
|
```
|
@ -33,6 +33,7 @@ from app.models import (
|
|||||||
Mailbox,
|
Mailbox,
|
||||||
DeletedAlias,
|
DeletedAlias,
|
||||||
DomainDeletedAlias,
|
DomainDeletedAlias,
|
||||||
|
PartnerUser,
|
||||||
)
|
)
|
||||||
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
|
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
|
||||||
|
|
||||||
@ -735,7 +736,8 @@ class InvalidMailboxDomainAdmin(SLModelView):
|
|||||||
class EmailSearchResult:
|
class EmailSearchResult:
|
||||||
no_match: bool = True
|
no_match: bool = True
|
||||||
alias: Optional[Alias] = None
|
alias: Optional[Alias] = None
|
||||||
mailbox: Optional[Mailbox] = None
|
mailbox: list[Mailbox] = []
|
||||||
|
mailbox_count: int = 0
|
||||||
deleted_alias: Optional[DeletedAlias] = None
|
deleted_alias: Optional[DeletedAlias] = None
|
||||||
deleted_custom_alias: Optional[DomainDeletedAlias] = None
|
deleted_custom_alias: Optional[DomainDeletedAlias] = None
|
||||||
user: Optional[User] = None
|
user: Optional[User] = None
|
||||||
@ -747,22 +749,21 @@ class EmailSearchResult:
|
|||||||
if alias:
|
if alias:
|
||||||
output.alias = alias
|
output.alias = alias
|
||||||
output.no_match = False
|
output.no_match = False
|
||||||
return output
|
|
||||||
user = User.get_by(email=email)
|
user = User.get_by(email=email)
|
||||||
if user:
|
if user:
|
||||||
output.user = user
|
output.user = user
|
||||||
output.no_match = False
|
output.no_match = False
|
||||||
return output
|
mailboxes = (
|
||||||
mailbox = Mailbox.get_by(email=email)
|
Mailbox.filter_by(email=email).order_by(Mailbox.id.desc()).limit(10).all()
|
||||||
if mailbox:
|
)
|
||||||
output.mailbox = mailbox
|
if mailboxes:
|
||||||
|
output.mailbox = mailboxes
|
||||||
|
output.mailbox_count = Mailbox.filter_by(email=email).count()
|
||||||
output.no_match = False
|
output.no_match = False
|
||||||
return output
|
|
||||||
deleted_alias = DeletedAlias.get_by(email=email)
|
deleted_alias = DeletedAlias.get_by(email=email)
|
||||||
if deleted_alias:
|
if deleted_alias:
|
||||||
output.deleted_alias = deleted_alias
|
output.deleted_alias = deleted_alias
|
||||||
output.no_match = False
|
output.no_match = False
|
||||||
return output
|
|
||||||
domain_deleted_alias = DomainDeletedAlias.get_by(email=email)
|
domain_deleted_alias = DomainDeletedAlias.get_by(email=email)
|
||||||
if domain_deleted_alias:
|
if domain_deleted_alias:
|
||||||
output.domain_deleted_alias = domain_deleted_alias
|
output.domain_deleted_alias = domain_deleted_alias
|
||||||
@ -782,16 +783,22 @@ class EmailSearchHelpers:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mailbox_count(user: User) -> int:
|
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
|
@staticmethod
|
||||||
def alias_list(user: User) -> list[Alias]:
|
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
|
@staticmethod
|
||||||
def alias_count(user: User) -> int:
|
def alias_count(user: User) -> int:
|
||||||
return Alias.filter_by(user_id=user.id).count()
|
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):
|
class EmailSearchAdmin(BaseView):
|
||||||
def is_accessible(self):
|
def is_accessible(self):
|
||||||
@ -805,9 +812,8 @@ class EmailSearchAdmin(BaseView):
|
|||||||
@expose("/", methods=["GET", "POST"])
|
@expose("/", methods=["GET", "POST"])
|
||||||
def index(self):
|
def index(self):
|
||||||
search = EmailSearchResult()
|
search = EmailSearchResult()
|
||||||
email = ""
|
email = request.args.get("email")
|
||||||
if request.form and request.form["email"]:
|
if email is not None and len(email) > 0:
|
||||||
email = request.form["email"]
|
|
||||||
email = email.strip()
|
email = email.strip()
|
||||||
search = EmailSearchResult.from_email(email)
|
search = EmailSearchResult.from_email(email)
|
||||||
|
|
||||||
|
@ -330,7 +330,10 @@ def try_auto_create_via_domain(address: str) -> Optional[Alias]:
|
|||||||
|
|
||||||
|
|
||||||
def delete_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
|
Delete an alias and add it to either global or domain trash
|
||||||
@ -360,12 +363,17 @@ def delete_alias(
|
|||||||
Session.commit()
|
Session.commit()
|
||||||
LOG.i(f"Moving {alias} to global trash {deleted_alias}")
|
LOG.i(f"Moving {alias} to global trash {deleted_alias}")
|
||||||
|
|
||||||
|
alias_id = alias.id
|
||||||
|
alias_email = alias.email
|
||||||
Alias.filter(Alias.id == alias.id).delete()
|
Alias.filter(Alias.id == alias.id).delete()
|
||||||
Session.commit()
|
Session.commit()
|
||||||
|
|
||||||
EventDispatcher.send_event(
|
EventDispatcher.send_event(
|
||||||
user, EventContent(alias_deleted=AliasDeleted(alias_id=alias.id))
|
user,
|
||||||
|
EventContent(alias_deleted=AliasDeleted(id=alias_id, email=alias_email)),
|
||||||
)
|
)
|
||||||
|
if commit:
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
|
|
||||||
def aliases_for_mailbox(mailbox: Mailbox) -> [Alias]:
|
def aliases_for_mailbox(mailbox: Mailbox) -> [Alias]:
|
||||||
@ -501,7 +509,10 @@ def change_alias_status(alias: Alias, enabled: bool, commit: bool = False):
|
|||||||
alias.enabled = enabled
|
alias.enabled = enabled
|
||||||
|
|
||||||
event = AliasStatusChanged(
|
event = AliasStatusChanged(
|
||||||
alias_id=alias.id, alias_email=alias.email, enabled=enabled
|
id=alias.id,
|
||||||
|
email=alias.email,
|
||||||
|
enabled=enabled,
|
||||||
|
created_at=int(alias.created_at.timestamp),
|
||||||
)
|
)
|
||||||
EventDispatcher.send_event(alias.user, EventContent(alias_status_change=event))
|
EventDispatcher.send_event(alias.user, EventContent(alias_status_change=event))
|
||||||
|
|
||||||
|
@ -35,6 +35,33 @@ def sl_getenv(env_var: str, default_factory: Callable = None):
|
|||||||
return literal_eval(value)
|
return literal_eval(value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_dict(env_var: str) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get an env variable and convert it into a python dictionary with keys and values as strings.
|
||||||
|
Args:
|
||||||
|
env_var (str): env var, example: SL_DB
|
||||||
|
|
||||||
|
Syntax is: key1=value1;key2=value2
|
||||||
|
Components separated by ;
|
||||||
|
key and value separated by =
|
||||||
|
"""
|
||||||
|
value = os.getenv(env_var)
|
||||||
|
if not value:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
components = value.split(";")
|
||||||
|
result = {}
|
||||||
|
for component in components:
|
||||||
|
if component == "":
|
||||||
|
continue
|
||||||
|
parts = component.split("=")
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise Exception(f"Invalid config for env var {env_var}")
|
||||||
|
result[parts[0].strip()] = parts[1].strip()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
config_file = os.environ.get("CONFIG")
|
config_file = os.environ.get("CONFIG")
|
||||||
if config_file:
|
if config_file:
|
||||||
config_file = get_abs_path(config_file)
|
config_file = get_abs_path(config_file)
|
||||||
@ -609,3 +636,24 @@ EVENT_WEBHOOK_ENABLED_USER_IDS: Optional[List[int]] = read_webhook_enabled_user_
|
|||||||
# Allow to define a different DB_URI for the event listener, in case we want to skip the connection pool
|
# 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
|
# 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)
|
EVENT_LISTENER_DB_URI = os.environ.get("EVENT_LISTENER_DB_URI", DB_URI)
|
||||||
|
|
||||||
|
|
||||||
|
def read_partner_dict(var: str) -> dict[int, str]:
|
||||||
|
partner_value = get_env_dict(var)
|
||||||
|
if len(partner_value) == 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
res: dict[int, str] = {}
|
||||||
|
for partner_id in partner_value.keys():
|
||||||
|
try:
|
||||||
|
partner_id_int = int(partner_id.strip())
|
||||||
|
res[partner_id_int] = partner_value[partner_id]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
PARTNER_DOMAINS: dict[int, str] = read_partner_dict("PARTNER_DOMAINS")
|
||||||
|
PARTNER_DOMAIN_VALIDATION_PREFIXES: dict[int, str] = read_partner_dict(
|
||||||
|
"PARTNER_DOMAIN_VALIDATION_PREFIXES"
|
||||||
|
)
|
||||||
|
@ -1 +1,2 @@
|
|||||||
HEADER_ALLOW_API_COOKIES = "X-Sl-Allowcookies"
|
HEADER_ALLOW_API_COOKIES = "X-Sl-Allowcookies"
|
||||||
|
DMARC_RECORD = "v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s"
|
||||||
|
128
app/app/custom_domain_utils.py
Normal file
128
app/app/custom_domain_utils.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from app.db import Session
|
||||||
|
from app.email_utils import get_email_domain_part
|
||||||
|
from app.log import LOG
|
||||||
|
from app.models import User, CustomDomain, SLDomain, Mailbox
|
||||||
|
|
||||||
|
_ALLOWED_DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CreateCustomDomainResult:
|
||||||
|
message: str = ""
|
||||||
|
message_category: str = ""
|
||||||
|
success: bool = False
|
||||||
|
instance: Optional[CustomDomain] = None
|
||||||
|
redirect: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CannotUseDomainReason(Enum):
|
||||||
|
InvalidDomain = 1
|
||||||
|
BuiltinDomain = 2
|
||||||
|
DomainAlreadyUsed = 3
|
||||||
|
DomainPartOfUserEmail = 4
|
||||||
|
DomainUserInMailbox = 5
|
||||||
|
|
||||||
|
def message(self, domain: str) -> str:
|
||||||
|
if self == CannotUseDomainReason.InvalidDomain:
|
||||||
|
return "This is not a valid domain"
|
||||||
|
elif self == CannotUseDomainReason.BuiltinDomain:
|
||||||
|
return "A custom domain cannot be a built-in domain."
|
||||||
|
elif self == CannotUseDomainReason.DomainAlreadyUsed:
|
||||||
|
return f"{domain} already used"
|
||||||
|
elif self == CannotUseDomainReason.DomainPartOfUserEmail:
|
||||||
|
return "You cannot add a domain that you are currently using for your personal email. Please change your personal email to your real email"
|
||||||
|
elif self == CannotUseDomainReason.DomainUserInMailbox:
|
||||||
|
return f"{domain} already used in a SimpleLogin mailbox"
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid CannotUseDomainReason")
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_domain(domain: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks that a domain is valid according to RFC 1035
|
||||||
|
"""
|
||||||
|
if len(domain) > 255:
|
||||||
|
return False
|
||||||
|
if domain.endswith("."):
|
||||||
|
domain = domain[:-1] # Strip the trailing dot
|
||||||
|
labels = domain.split(".")
|
||||||
|
if not labels:
|
||||||
|
return False
|
||||||
|
for label in labels:
|
||||||
|
if not _ALLOWED_DOMAIN_REGEX.match(label):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_domain(domain: str) -> str:
|
||||||
|
new_domain = domain.lower().strip()
|
||||||
|
if new_domain.startswith("http://"):
|
||||||
|
new_domain = new_domain[len("http://") :]
|
||||||
|
|
||||||
|
if new_domain.startswith("https://"):
|
||||||
|
new_domain = new_domain[len("https://") :]
|
||||||
|
|
||||||
|
return new_domain
|
||||||
|
|
||||||
|
|
||||||
|
def can_domain_be_used(user: User, domain: str) -> Optional[CannotUseDomainReason]:
|
||||||
|
if not is_valid_domain(domain):
|
||||||
|
return CannotUseDomainReason.InvalidDomain
|
||||||
|
elif SLDomain.get_by(domain=domain):
|
||||||
|
return CannotUseDomainReason.BuiltinDomain
|
||||||
|
elif CustomDomain.get_by(domain=domain):
|
||||||
|
return CannotUseDomainReason.DomainAlreadyUsed
|
||||||
|
elif get_email_domain_part(user.email) == domain:
|
||||||
|
return CannotUseDomainReason.DomainPartOfUserEmail
|
||||||
|
elif Mailbox.filter(
|
||||||
|
Mailbox.verified.is_(True), Mailbox.email.endswith(f"@{domain}")
|
||||||
|
).first():
|
||||||
|
return CannotUseDomainReason.DomainUserInMailbox
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def create_custom_domain(
|
||||||
|
user: User, domain: str, partner_id: Optional[int] = None
|
||||||
|
) -> CreateCustomDomainResult:
|
||||||
|
if not user.is_premium():
|
||||||
|
return CreateCustomDomainResult(
|
||||||
|
message="Only premium plan can add custom domain",
|
||||||
|
message_category="warning",
|
||||||
|
)
|
||||||
|
|
||||||
|
new_domain = sanitize_domain(domain)
|
||||||
|
domain_forbidden_cause = can_domain_be_used(user, new_domain)
|
||||||
|
if domain_forbidden_cause:
|
||||||
|
return CreateCustomDomainResult(
|
||||||
|
message=domain_forbidden_cause.message(new_domain), message_category="error"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_custom_domain = CustomDomain.create(domain=new_domain, user_id=user.id)
|
||||||
|
|
||||||
|
# new domain has ownership verified if its parent has the ownership verified
|
||||||
|
for root_cd in user.custom_domains:
|
||||||
|
if new_domain.endswith("." + root_cd.domain) and root_cd.ownership_verified:
|
||||||
|
LOG.i(
|
||||||
|
"%s ownership verified thanks to %s",
|
||||||
|
new_custom_domain,
|
||||||
|
root_cd,
|
||||||
|
)
|
||||||
|
new_custom_domain.ownership_verified = True
|
||||||
|
|
||||||
|
# Add the partner_id in case it's passed
|
||||||
|
if partner_id is not None:
|
||||||
|
new_custom_domain.partner_id = partner_id
|
||||||
|
|
||||||
|
Session.commit()
|
||||||
|
|
||||||
|
return CreateCustomDomainResult(
|
||||||
|
success=True,
|
||||||
|
instance=new_custom_domain,
|
||||||
|
)
|
@ -1,37 +1,157 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from app import config
|
||||||
|
from app.constants import DMARC_RECORD
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.dns_utils import get_cname_record
|
from app.dns_utils import (
|
||||||
|
DNSClient,
|
||||||
|
is_mx_equivalent,
|
||||||
|
get_network_dns_client,
|
||||||
|
)
|
||||||
from app.models import CustomDomain
|
from app.models import CustomDomain
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DomainValidationResult:
|
||||||
|
success: bool
|
||||||
|
errors: [str]
|
||||||
|
|
||||||
|
|
||||||
class CustomDomainValidation:
|
class CustomDomainValidation:
|
||||||
def __init__(self, dkim_domain: str):
|
def __init__(
|
||||||
|
self,
|
||||||
|
dkim_domain: str,
|
||||||
|
dns_client: DNSClient = get_network_dns_client(),
|
||||||
|
partner_domains: Optional[dict[int, str]] = None,
|
||||||
|
partner_domains_validation_prefixes: Optional[dict[int, str]] = None,
|
||||||
|
):
|
||||||
self.dkim_domain = dkim_domain
|
self.dkim_domain = dkim_domain
|
||||||
self._dkim_records = {
|
self._dns_client = dns_client
|
||||||
(f"{key}._domainkey", f"{key}._domainkey.{self.dkim_domain}")
|
self._partner_domains = partner_domains or config.PARTNER_DOMAINS
|
||||||
|
self._partner_domain_validation_prefixes = (
|
||||||
|
partner_domains_validation_prefixes
|
||||||
|
or config.PARTNER_DOMAIN_VALIDATION_PREFIXES
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_ownership_verification_record(self, domain: CustomDomain) -> str:
|
||||||
|
prefix = "sl"
|
||||||
|
if (
|
||||||
|
domain.partner_id is not None
|
||||||
|
and domain.partner_id in self._partner_domain_validation_prefixes
|
||||||
|
):
|
||||||
|
prefix = self._partner_domain_validation_prefixes[domain.partner_id]
|
||||||
|
return f"{prefix}-verification={domain.ownership_txt_token}"
|
||||||
|
|
||||||
|
def get_dkim_records(self, domain: CustomDomain) -> {str: str}:
|
||||||
|
"""
|
||||||
|
Get a list of dkim records to set up. Depending on the custom_domain, whether if it's from a partner or not,
|
||||||
|
it will return the default ones or the partner ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# By default use the default domain
|
||||||
|
dkim_domain = self.dkim_domain
|
||||||
|
if domain.partner_id is not None:
|
||||||
|
# Domain is from a partner. Retrieve the partner config and use that domain if exists
|
||||||
|
dkim_domain = self._partner_domains.get(domain.partner_id, dkim_domain)
|
||||||
|
|
||||||
|
return {
|
||||||
|
f"{key}._domainkey": f"{key}._domainkey.{dkim_domain}"
|
||||||
for key in ("dkim", "dkim02", "dkim03")
|
for key in ("dkim", "dkim02", "dkim03")
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_dkim_records(self) -> {str: str}:
|
|
||||||
"""
|
|
||||||
Get a list of dkim records to set up. It will be
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._dkim_records
|
|
||||||
|
|
||||||
def validate_dkim_records(self, custom_domain: CustomDomain) -> dict[str, str]:
|
def validate_dkim_records(self, custom_domain: CustomDomain) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Check if dkim records are properly set for this custom domain.
|
Check if dkim records are properly set for this custom domain.
|
||||||
Returns empty list if all records are ok. Other-wise return the records that aren't properly configured
|
Returns empty list if all records are ok. Other-wise return the records that aren't properly configured
|
||||||
"""
|
"""
|
||||||
|
correct_records = {}
|
||||||
invalid_records = {}
|
invalid_records = {}
|
||||||
for prefix, expected_record in self.get_dkim_records():
|
expected_records = self.get_dkim_records(custom_domain)
|
||||||
|
for prefix, expected_record in expected_records.items():
|
||||||
custom_record = f"{prefix}.{custom_domain.domain}"
|
custom_record = f"{prefix}.{custom_domain.domain}"
|
||||||
dkim_record = get_cname_record(custom_record)
|
dkim_record = self._dns_client.get_cname_record(custom_record)
|
||||||
if dkim_record != expected_record:
|
if dkim_record == expected_record:
|
||||||
|
correct_records[prefix] = custom_record
|
||||||
|
else:
|
||||||
invalid_records[custom_record] = dkim_record or "empty"
|
invalid_records[custom_record] = dkim_record or "empty"
|
||||||
# HACK: If dkim is enabled, don't disable it to give users time to update their CNAMES
|
|
||||||
|
# HACK
|
||||||
|
# As initially we only had one dkim record, we want to allow users that had only the original dkim record and
|
||||||
|
# the domain validated to continue seeing it as validated (although showing them the missing records).
|
||||||
|
# However, if not even the original dkim record is right, even if the domain was dkim_verified in the past,
|
||||||
|
# we will remove the dkim_verified flag.
|
||||||
|
# This is done in order to give users with the old dkim config (only one) to update their CNAMEs
|
||||||
if custom_domain.dkim_verified:
|
if custom_domain.dkim_verified:
|
||||||
return invalid_records
|
# Check if at least the original dkim is there
|
||||||
|
if correct_records.get("dkim._domainkey") is not None:
|
||||||
|
# Original dkim record is there. Return the missing records (if any) and don't clear the flag
|
||||||
|
return invalid_records
|
||||||
|
|
||||||
|
# Original DKIM record is not there, which means the DKIM config is not finished. Proceed with the
|
||||||
|
# rest of the code path, returning the invalid records and clearing the flag
|
||||||
custom_domain.dkim_verified = len(invalid_records) == 0
|
custom_domain.dkim_verified = len(invalid_records) == 0
|
||||||
Session.commit()
|
Session.commit()
|
||||||
return invalid_records
|
return invalid_records
|
||||||
|
|
||||||
|
def validate_domain_ownership(
|
||||||
|
self, custom_domain: CustomDomain
|
||||||
|
) -> DomainValidationResult:
|
||||||
|
"""
|
||||||
|
Check if the custom_domain has added the ownership verification records
|
||||||
|
"""
|
||||||
|
txt_records = self._dns_client.get_txt_record(custom_domain.domain)
|
||||||
|
expected_verification_record = self.get_ownership_verification_record(
|
||||||
|
custom_domain
|
||||||
|
)
|
||||||
|
|
||||||
|
if expected_verification_record in txt_records:
|
||||||
|
custom_domain.ownership_verified = True
|
||||||
|
Session.commit()
|
||||||
|
return DomainValidationResult(success=True, errors=[])
|
||||||
|
else:
|
||||||
|
return DomainValidationResult(success=False, errors=txt_records)
|
||||||
|
|
||||||
|
def validate_mx_records(
|
||||||
|
self, custom_domain: CustomDomain
|
||||||
|
) -> DomainValidationResult:
|
||||||
|
mx_domains = self._dns_client.get_mx_domains(custom_domain.domain)
|
||||||
|
|
||||||
|
if not is_mx_equivalent(mx_domains, config.EMAIL_SERVERS_WITH_PRIORITY):
|
||||||
|
return DomainValidationResult(
|
||||||
|
success=False,
|
||||||
|
errors=[f"{priority} {domain}" for (priority, domain) in mx_domains],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
custom_domain.verified = True
|
||||||
|
Session.commit()
|
||||||
|
return DomainValidationResult(success=True, errors=[])
|
||||||
|
|
||||||
|
def validate_spf_records(
|
||||||
|
self, custom_domain: CustomDomain
|
||||||
|
) -> DomainValidationResult:
|
||||||
|
spf_domains = self._dns_client.get_spf_domain(custom_domain.domain)
|
||||||
|
if config.EMAIL_DOMAIN in spf_domains:
|
||||||
|
custom_domain.spf_verified = True
|
||||||
|
Session.commit()
|
||||||
|
return DomainValidationResult(success=True, errors=[])
|
||||||
|
else:
|
||||||
|
custom_domain.spf_verified = False
|
||||||
|
Session.commit()
|
||||||
|
return DomainValidationResult(
|
||||||
|
success=False,
|
||||||
|
errors=self._dns_client.get_txt_record(custom_domain.domain),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_dmarc_records(
|
||||||
|
self, custom_domain: CustomDomain
|
||||||
|
) -> DomainValidationResult:
|
||||||
|
txt_records = self._dns_client.get_txt_record("_dmarc." + custom_domain.domain)
|
||||||
|
if DMARC_RECORD in txt_records:
|
||||||
|
custom_domain.dmarc_verified = True
|
||||||
|
Session.commit()
|
||||||
|
return DomainValidationResult(success=True, errors=[])
|
||||||
|
else:
|
||||||
|
custom_domain.dmarc_verified = False
|
||||||
|
Session.commit()
|
||||||
|
return DomainValidationResult(success=False, errors=txt_records)
|
||||||
|
@ -5,11 +5,9 @@ from wtforms import StringField, validators
|
|||||||
|
|
||||||
from app import parallel_limiter
|
from app import parallel_limiter
|
||||||
from app.config import EMAIL_SERVERS_WITH_PRIORITY
|
from app.config import EMAIL_SERVERS_WITH_PRIORITY
|
||||||
|
from app.custom_domain_utils import create_custom_domain
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.db import Session
|
from app.models import CustomDomain
|
||||||
from app.email_utils import get_email_domain_part
|
|
||||||
from app.log import LOG
|
|
||||||
from app.models import CustomDomain, Mailbox, DomainMailbox, SLDomain
|
|
||||||
|
|
||||||
|
|
||||||
class NewCustomDomainForm(FlaskForm):
|
class NewCustomDomainForm(FlaskForm):
|
||||||
@ -25,11 +23,8 @@ def custom_domain():
|
|||||||
custom_domains = CustomDomain.filter_by(
|
custom_domains = CustomDomain.filter_by(
|
||||||
user_id=current_user.id, is_sl_subdomain=False
|
user_id=current_user.id, is_sl_subdomain=False
|
||||||
).all()
|
).all()
|
||||||
mailboxes = current_user.mailboxes()
|
|
||||||
new_custom_domain_form = NewCustomDomainForm()
|
new_custom_domain_form = NewCustomDomainForm()
|
||||||
|
|
||||||
errors = {}
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if request.form.get("form-name") == "create":
|
if request.form.get("form-name") == "create":
|
||||||
if not current_user.is_premium():
|
if not current_user.is_premium():
|
||||||
@ -37,87 +32,25 @@ def custom_domain():
|
|||||||
return redirect(url_for("dashboard.custom_domain"))
|
return redirect(url_for("dashboard.custom_domain"))
|
||||||
|
|
||||||
if new_custom_domain_form.validate():
|
if new_custom_domain_form.validate():
|
||||||
new_domain = new_custom_domain_form.domain.data.lower().strip()
|
res = create_custom_domain(
|
||||||
|
user=current_user, domain=new_custom_domain_form.domain.data
|
||||||
if new_domain.startswith("http://"):
|
)
|
||||||
new_domain = new_domain[len("http://") :]
|
if res.success:
|
||||||
|
flash(f"New domain {res.instance.domain} is created", "success")
|
||||||
if new_domain.startswith("https://"):
|
|
||||||
new_domain = new_domain[len("https://") :]
|
|
||||||
|
|
||||||
if SLDomain.get_by(domain=new_domain):
|
|
||||||
flash("A custom domain cannot be a built-in domain.", "error")
|
|
||||||
elif CustomDomain.get_by(domain=new_domain):
|
|
||||||
flash(f"{new_domain} already used", "error")
|
|
||||||
elif get_email_domain_part(current_user.email) == new_domain:
|
|
||||||
flash(
|
|
||||||
"You cannot add a domain that you are currently using for your personal email. "
|
|
||||||
"Please change your personal email to your real email",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
elif Mailbox.filter(
|
|
||||||
Mailbox.verified.is_(True), Mailbox.email.endswith(f"@{new_domain}")
|
|
||||||
).first():
|
|
||||||
flash(
|
|
||||||
f"{new_domain} already used in a SimpleLogin mailbox", "error"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
new_custom_domain = CustomDomain.create(
|
|
||||||
domain=new_domain, user_id=current_user.id
|
|
||||||
)
|
|
||||||
# new domain has ownership verified if its parent has the ownership verified
|
|
||||||
for root_cd in current_user.custom_domains:
|
|
||||||
if (
|
|
||||||
new_domain.endswith("." + root_cd.domain)
|
|
||||||
and root_cd.ownership_verified
|
|
||||||
):
|
|
||||||
LOG.i(
|
|
||||||
"%s ownership verified thanks to %s",
|
|
||||||
new_custom_domain,
|
|
||||||
root_cd,
|
|
||||||
)
|
|
||||||
new_custom_domain.ownership_verified = True
|
|
||||||
|
|
||||||
Session.commit()
|
|
||||||
|
|
||||||
mailbox_ids = request.form.getlist("mailbox_ids")
|
|
||||||
if mailbox_ids:
|
|
||||||
# check if mailbox is not tempered with
|
|
||||||
mailboxes = []
|
|
||||||
for mailbox_id in mailbox_ids:
|
|
||||||
mailbox = Mailbox.get(mailbox_id)
|
|
||||||
if (
|
|
||||||
not mailbox
|
|
||||||
or mailbox.user_id != current_user.id
|
|
||||||
or not mailbox.verified
|
|
||||||
):
|
|
||||||
flash("Something went wrong, please retry", "warning")
|
|
||||||
return redirect(url_for("dashboard.custom_domain"))
|
|
||||||
mailboxes.append(mailbox)
|
|
||||||
|
|
||||||
for mailbox in mailboxes:
|
|
||||||
DomainMailbox.create(
|
|
||||||
domain_id=new_custom_domain.id, mailbox_id=mailbox.id
|
|
||||||
)
|
|
||||||
|
|
||||||
Session.commit()
|
|
||||||
|
|
||||||
flash(
|
|
||||||
f"New domain {new_custom_domain.domain} is created", "success"
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.domain_detail_dns",
|
"dashboard.domain_detail_dns",
|
||||||
custom_domain_id=new_custom_domain.id,
|
custom_domain_id=res.instance.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
flash(res.message, res.message_category)
|
||||||
|
if res.redirect:
|
||||||
|
return redirect(url_for(res.redirect))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/custom_domain.html",
|
"dashboard/custom_domain.html",
|
||||||
custom_domains=custom_domains,
|
custom_domains=custom_domains,
|
||||||
new_custom_domain_form=new_custom_domain_form,
|
new_custom_domain_form=new_custom_domain_form,
|
||||||
EMAIL_SERVERS_WITH_PRIORITY=EMAIL_SERVERS_WITH_PRIORITY,
|
EMAIL_SERVERS_WITH_PRIORITY=EMAIL_SERVERS_WITH_PRIORITY,
|
||||||
errors=errors,
|
|
||||||
mailboxes=mailboxes,
|
|
||||||
)
|
)
|
||||||
|
@ -6,16 +6,11 @@ from flask_login import login_required, current_user
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, validators, IntegerField
|
from wtforms import StringField, validators, IntegerField
|
||||||
|
|
||||||
|
from app.constants import DMARC_RECORD
|
||||||
from app.config import EMAIL_SERVERS_WITH_PRIORITY, EMAIL_DOMAIN, JOB_DELETE_DOMAIN
|
from app.config import EMAIL_SERVERS_WITH_PRIORITY, EMAIL_DOMAIN, JOB_DELETE_DOMAIN
|
||||||
from app.custom_domain_validation import CustomDomainValidation
|
from app.custom_domain_validation import CustomDomainValidation
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.dns_utils import (
|
|
||||||
get_mx_domains,
|
|
||||||
get_spf_domain,
|
|
||||||
get_txt_record,
|
|
||||||
is_mx_equivalent,
|
|
||||||
)
|
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import (
|
from app.models import (
|
||||||
CustomDomain,
|
CustomDomain,
|
||||||
@ -49,8 +44,6 @@ def domain_detail_dns(custom_domain_id):
|
|||||||
domain_validator = CustomDomainValidation(EMAIL_DOMAIN)
|
domain_validator = CustomDomainValidation(EMAIL_DOMAIN)
|
||||||
csrf_form = CSRFValidationForm()
|
csrf_form = CSRFValidationForm()
|
||||||
|
|
||||||
dmarc_record = "v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s"
|
|
||||||
|
|
||||||
mx_ok = spf_ok = dkim_ok = dmarc_ok = ownership_ok = True
|
mx_ok = spf_ok = dkim_ok = dmarc_ok = ownership_ok = True
|
||||||
mx_errors = spf_errors = dkim_errors = dmarc_errors = ownership_errors = []
|
mx_errors = spf_errors = dkim_errors = dmarc_errors = ownership_errors = []
|
||||||
|
|
||||||
@ -59,15 +52,14 @@ def domain_detail_dns(custom_domain_id):
|
|||||||
flash("Invalid request", "warning")
|
flash("Invalid request", "warning")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
if request.form.get("form-name") == "check-ownership":
|
if request.form.get("form-name") == "check-ownership":
|
||||||
txt_records = get_txt_record(custom_domain.domain)
|
ownership_validation_result = domain_validator.validate_domain_ownership(
|
||||||
|
custom_domain
|
||||||
if custom_domain.get_ownership_dns_txt_value() in txt_records:
|
)
|
||||||
|
if ownership_validation_result.success:
|
||||||
flash(
|
flash(
|
||||||
"Domain ownership is verified. Please proceed to the other records setup",
|
"Domain ownership is verified. Please proceed to the other records setup",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
custom_domain.ownership_verified = True
|
|
||||||
Session.commit()
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.domain_detail_dns",
|
"dashboard.domain_detail_dns",
|
||||||
@ -78,36 +70,28 @@ def domain_detail_dns(custom_domain_id):
|
|||||||
else:
|
else:
|
||||||
flash("We can't find the needed TXT record", "error")
|
flash("We can't find the needed TXT record", "error")
|
||||||
ownership_ok = False
|
ownership_ok = False
|
||||||
ownership_errors = txt_records
|
ownership_errors = ownership_validation_result.errors
|
||||||
|
|
||||||
elif request.form.get("form-name") == "check-mx":
|
elif request.form.get("form-name") == "check-mx":
|
||||||
mx_domains = get_mx_domains(custom_domain.domain)
|
mx_validation_result = domain_validator.validate_mx_records(custom_domain)
|
||||||
|
if mx_validation_result.success:
|
||||||
if not is_mx_equivalent(mx_domains, EMAIL_SERVERS_WITH_PRIORITY):
|
|
||||||
flash("The MX record is not correctly set", "warning")
|
|
||||||
|
|
||||||
mx_ok = False
|
|
||||||
# build mx_errors to show to user
|
|
||||||
mx_errors = [
|
|
||||||
f"{priority} {domain}" for (priority, domain) in mx_domains
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
flash(
|
flash(
|
||||||
"Your domain can start receiving emails. You can now use it to create alias",
|
"Your domain can start receiving emails. You can now use it to create alias",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
custom_domain.verified = True
|
|
||||||
Session.commit()
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"dashboard.domain_detail_dns", custom_domain_id=custom_domain.id
|
"dashboard.domain_detail_dns", custom_domain_id=custom_domain.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
flash("The MX record is not correctly set", "warning")
|
||||||
|
mx_ok = False
|
||||||
|
mx_errors = mx_validation_result.errors
|
||||||
|
|
||||||
elif request.form.get("form-name") == "check-spf":
|
elif request.form.get("form-name") == "check-spf":
|
||||||
spf_domains = get_spf_domain(custom_domain.domain)
|
spf_validation_result = domain_validator.validate_spf_records(custom_domain)
|
||||||
if EMAIL_DOMAIN in spf_domains:
|
if spf_validation_result.success:
|
||||||
custom_domain.spf_verified = True
|
|
||||||
Session.commit()
|
|
||||||
flash("SPF is setup correctly", "success")
|
flash("SPF is setup correctly", "success")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
@ -115,14 +99,12 @@ def domain_detail_dns(custom_domain_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
custom_domain.spf_verified = False
|
|
||||||
Session.commit()
|
|
||||||
flash(
|
flash(
|
||||||
f"SPF: {EMAIL_DOMAIN} is not included in your SPF record.",
|
f"SPF: {EMAIL_DOMAIN} is not included in your SPF record.",
|
||||||
"warning",
|
"warning",
|
||||||
)
|
)
|
||||||
spf_ok = False
|
spf_ok = False
|
||||||
spf_errors = get_txt_record(custom_domain.domain)
|
spf_errors = spf_validation_result.errors
|
||||||
|
|
||||||
elif request.form.get("form-name") == "check-dkim":
|
elif request.form.get("form-name") == "check-dkim":
|
||||||
dkim_errors = domain_validator.validate_dkim_records(custom_domain)
|
dkim_errors = domain_validator.validate_dkim_records(custom_domain)
|
||||||
@ -138,10 +120,10 @@ def domain_detail_dns(custom_domain_id):
|
|||||||
flash("DKIM: the CNAME record is not correctly set", "warning")
|
flash("DKIM: the CNAME record is not correctly set", "warning")
|
||||||
|
|
||||||
elif request.form.get("form-name") == "check-dmarc":
|
elif request.form.get("form-name") == "check-dmarc":
|
||||||
txt_records = get_txt_record("_dmarc." + custom_domain.domain)
|
dmarc_validation_result = domain_validator.validate_dmarc_records(
|
||||||
if dmarc_record in txt_records:
|
custom_domain
|
||||||
custom_domain.dmarc_verified = True
|
)
|
||||||
Session.commit()
|
if dmarc_validation_result.success:
|
||||||
flash("DMARC is setup correctly", "success")
|
flash("DMARC is setup correctly", "success")
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
@ -149,19 +131,21 @@ def domain_detail_dns(custom_domain_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
custom_domain.dmarc_verified = False
|
|
||||||
Session.commit()
|
|
||||||
flash(
|
flash(
|
||||||
"DMARC: The TXT record is not correctly set",
|
"DMARC: The TXT record is not correctly set",
|
||||||
"warning",
|
"warning",
|
||||||
)
|
)
|
||||||
dmarc_ok = False
|
dmarc_ok = False
|
||||||
dmarc_errors = txt_records
|
dmarc_errors = dmarc_validation_result.errors
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/domain_detail/dns.html",
|
"dashboard/domain_detail/dns.html",
|
||||||
EMAIL_SERVERS_WITH_PRIORITY=EMAIL_SERVERS_WITH_PRIORITY,
|
EMAIL_SERVERS_WITH_PRIORITY=EMAIL_SERVERS_WITH_PRIORITY,
|
||||||
dkim_records=domain_validator.get_dkim_records(),
|
ownership_record=domain_validator.get_ownership_verification_record(
|
||||||
|
custom_domain
|
||||||
|
),
|
||||||
|
dkim_records=domain_validator.get_dkim_records(custom_domain),
|
||||||
|
dmarc_record=DMARC_RECORD,
|
||||||
**locals(),
|
**locals(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ def index():
|
|||||||
LOG.i(f"User {current_user} requested deletion of alias {alias}")
|
LOG.i(f"User {current_user} requested deletion of alias {alias}")
|
||||||
email = alias.email
|
email = alias.email
|
||||||
alias_utils.delete_alias(
|
alias_utils.delete_alias(
|
||||||
alias, current_user, AliasDeleteReason.ManualAction
|
alias, current_user, AliasDeleteReason.ManualAction, commit=True
|
||||||
)
|
)
|
||||||
flash(f"Alias {email} has been deleted", "success")
|
flash(f"Alias {email} has been deleted", "success")
|
||||||
elif request.form.get("form-name") == "disable-alias":
|
elif request.form.get("form-name") == "disable-alias":
|
||||||
|
@ -123,7 +123,12 @@ def mailbox_verify():
|
|||||||
if not code:
|
if not code:
|
||||||
# Old way
|
# Old way
|
||||||
return verify_with_signed_secret(mailbox_id)
|
return verify_with_signed_secret(mailbox_id)
|
||||||
mailbox = mailbox_utils.verify_mailbox_code(current_user, mailbox_id, code)
|
try:
|
||||||
|
mailbox = mailbox_utils.verify_mailbox_code(current_user, mailbox_id, code)
|
||||||
|
except mailbox_utils.MailboxError as e:
|
||||||
|
LOG.i(f"Cannot verify mailbox {mailbox_id} because of {e}")
|
||||||
|
flash(f"Cannot verify mailbox: {e.msg}", "error")
|
||||||
|
return redirect(url_for("dashboard.mailbox_route"))
|
||||||
LOG.d("Mailbox %s is verified", mailbox)
|
LOG.d("Mailbox %s is verified", mailbox)
|
||||||
return render_template("dashboard/mailbox_validation.html", mailbox=mailbox)
|
return render_template("dashboard/mailbox_validation.html", mailbox=mailbox)
|
||||||
|
|
||||||
|
@ -1,100 +1,13 @@
|
|||||||
from app import config
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional, List, Tuple
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
|
|
||||||
|
from app.config import NAMESERVERS
|
||||||
def _get_dns_resolver():
|
|
||||||
my_resolver = dns.resolver.Resolver()
|
|
||||||
my_resolver.nameservers = config.NAMESERVERS
|
|
||||||
|
|
||||||
return my_resolver
|
|
||||||
|
|
||||||
|
|
||||||
def get_ns(hostname) -> [str]:
|
|
||||||
try:
|
|
||||||
answers = _get_dns_resolver().resolve(hostname, "NS", search=True)
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
return [a.to_text() for a in answers]
|
|
||||||
|
|
||||||
|
|
||||||
def get_cname_record(hostname) -> Optional[str]:
|
|
||||||
"""Return the CNAME record if exists for a domain, WITHOUT the trailing period at the end"""
|
|
||||||
try:
|
|
||||||
answers = _get_dns_resolver().resolve(hostname, "CNAME", search=True)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for a in answers:
|
|
||||||
ret = a.to_text()
|
|
||||||
return ret[:-1]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_mx_domains(hostname) -> [(int, str)]:
|
|
||||||
"""return list of (priority, domain name) sorted by priority (lowest priority first)
|
|
||||||
domain name ends with a "." at the end.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
answers = _get_dns_resolver().resolve(hostname, "MX", search=True)
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
for a in answers:
|
|
||||||
record = a.to_text() # for ex '20 alt2.aspmx.l.google.com.'
|
|
||||||
parts = record.split(" ")
|
|
||||||
|
|
||||||
ret.append((int(parts[0]), parts[1]))
|
|
||||||
|
|
||||||
return sorted(ret, key=lambda prio_domain: prio_domain[0])
|
|
||||||
|
|
||||||
|
|
||||||
_include_spf = "include:"
|
_include_spf = "include:"
|
||||||
|
|
||||||
|
|
||||||
def get_spf_domain(hostname) -> [str]:
|
|
||||||
"""return all domains listed in *include:*"""
|
|
||||||
try:
|
|
||||||
answers = _get_dns_resolver().resolve(hostname, "TXT", search=True)
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
for a in answers: # type: dns.rdtypes.ANY.TXT.TXT
|
|
||||||
for record in a.strings:
|
|
||||||
record = record.decode() # record is bytes
|
|
||||||
|
|
||||||
if record.startswith("v=spf1"):
|
|
||||||
parts = record.split(" ")
|
|
||||||
for part in parts:
|
|
||||||
if part.startswith(_include_spf):
|
|
||||||
ret.append(part[part.find(_include_spf) + len(_include_spf) :])
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def get_txt_record(hostname) -> [str]:
|
|
||||||
try:
|
|
||||||
answers = _get_dns_resolver().resolve(hostname, "TXT", search=True)
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
for a in answers: # type: dns.rdtypes.ANY.TXT.TXT
|
|
||||||
for record in a.strings:
|
|
||||||
record = record.decode() # record is bytes
|
|
||||||
|
|
||||||
ret.append(record)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def is_mx_equivalent(
|
def is_mx_equivalent(
|
||||||
mx_domains: List[Tuple[int, str]], ref_mx_domains: List[Tuple[int, str]]
|
mx_domains: List[Tuple[int, str]], ref_mx_domains: List[Tuple[int, str]]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@ -105,16 +18,127 @@ def is_mx_equivalent(
|
|||||||
The priority order is taken into account but not the priority number.
|
The priority order is taken into account but not the priority number.
|
||||||
For example, [(1, domain1), (2, domain2)] is equivalent to [(10, domain1), (20, domain2)]
|
For example, [(1, domain1), (2, domain2)] is equivalent to [(10, domain1), (20, domain2)]
|
||||||
"""
|
"""
|
||||||
mx_domains = sorted(mx_domains, key=lambda priority_domain: priority_domain[0])
|
mx_domains = sorted(mx_domains, key=lambda x: x[0])
|
||||||
ref_mx_domains = sorted(
|
ref_mx_domains = sorted(ref_mx_domains, key=lambda x: x[0])
|
||||||
ref_mx_domains, key=lambda priority_domain: priority_domain[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(mx_domains) < len(ref_mx_domains):
|
if len(mx_domains) < len(ref_mx_domains):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for i in range(0, len(ref_mx_domains)):
|
for i in range(len(ref_mx_domains)):
|
||||||
if mx_domains[i][1] != ref_mx_domains[i][1]:
|
if mx_domains[i][1] != ref_mx_domains[i][1]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DNSClient(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def get_cname_record(self, hostname: str) -> Optional[str]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_mx_domains(self, hostname: str) -> List[Tuple[int, str]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_spf_domain(self, hostname: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
return all domains listed in *include:*
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
records = self.get_txt_record(hostname)
|
||||||
|
ret = []
|
||||||
|
for record in records:
|
||||||
|
if record.startswith("v=spf1"):
|
||||||
|
parts = record.split(" ")
|
||||||
|
for part in parts:
|
||||||
|
if part.startswith(_include_spf):
|
||||||
|
ret.append(
|
||||||
|
part[part.find(_include_spf) + len(_include_spf) :]
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_txt_record(self, hostname: str) -> List[str]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkDNSClient(DNSClient):
|
||||||
|
def __init__(self, nameservers: List[str]):
|
||||||
|
self._resolver = dns.resolver.Resolver()
|
||||||
|
self._resolver.nameservers = nameservers
|
||||||
|
|
||||||
|
def get_cname_record(self, hostname: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Return the CNAME record if exists for a domain, WITHOUT the trailing period at the end
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
answers = self._resolver.resolve(hostname, "CNAME", search=True)
|
||||||
|
for a in answers:
|
||||||
|
ret = a.to_text()
|
||||||
|
return ret[:-1]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_mx_domains(self, hostname: str) -> List[Tuple[int, str]]:
|
||||||
|
"""
|
||||||
|
return list of (priority, domain name) sorted by priority (lowest priority first)
|
||||||
|
domain name ends with a "." at the end.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
answers = self._resolver.resolve(hostname, "MX", search=True)
|
||||||
|
ret = []
|
||||||
|
for a in answers:
|
||||||
|
record = a.to_text() # for ex '20 alt2.aspmx.l.google.com.'
|
||||||
|
parts = record.split(" ")
|
||||||
|
ret.append((int(parts[0]), parts[1]))
|
||||||
|
return sorted(ret, key=lambda x: x[0])
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_txt_record(self, hostname: str) -> List[str]:
|
||||||
|
try:
|
||||||
|
answers = self._resolver.resolve(hostname, "TXT", search=True)
|
||||||
|
ret = []
|
||||||
|
for a in answers: # type: dns.rdtypes.ANY.TXT.TXT
|
||||||
|
for record in a.strings:
|
||||||
|
ret.append(record.decode())
|
||||||
|
return ret
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryDNSClient(DNSClient):
|
||||||
|
def __init__(self):
|
||||||
|
self.cname_records: dict[str, Optional[str]] = {}
|
||||||
|
self.mx_records: dict[str, List[Tuple[int, str]]] = {}
|
||||||
|
self.spf_records: dict[str, List[str]] = {}
|
||||||
|
self.txt_records: dict[str, List[str]] = {}
|
||||||
|
|
||||||
|
def set_cname_record(self, hostname: str, cname: str):
|
||||||
|
self.cname_records[hostname] = cname
|
||||||
|
|
||||||
|
def set_mx_records(self, hostname: str, mx_list: List[Tuple[int, str]]):
|
||||||
|
self.mx_records[hostname] = mx_list
|
||||||
|
|
||||||
|
def set_txt_record(self, hostname: str, txt_list: List[str]):
|
||||||
|
self.txt_records[hostname] = txt_list
|
||||||
|
|
||||||
|
def get_cname_record(self, hostname: str) -> Optional[str]:
|
||||||
|
return self.cname_records.get(hostname)
|
||||||
|
|
||||||
|
def get_mx_domains(self, hostname: str) -> List[Tuple[int, str]]:
|
||||||
|
mx_list = self.mx_records.get(hostname, [])
|
||||||
|
return sorted(mx_list, key=lambda x: x[0])
|
||||||
|
|
||||||
|
def get_txt_record(self, hostname: str) -> List[str]:
|
||||||
|
return self.txt_records.get(hostname, [])
|
||||||
|
|
||||||
|
|
||||||
|
def get_network_dns_client() -> NetworkDNSClient:
|
||||||
|
return NetworkDNSClient(NAMESERVERS)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mx_domains(hostname: str) -> [(int, str)]:
|
||||||
|
return get_network_dns_client().get_mx_domains(hostname)
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
import newrelic.agent
|
||||||
|
|
||||||
from app import config
|
from app import config
|
||||||
from app.db import Session
|
from app.db import Session
|
||||||
from app.errors import ProtonPartnerNotSetUp
|
from app.errors import ProtonPartnerNotSetUp
|
||||||
from app.events.generated import event_pb2
|
from app.events.generated import event_pb2
|
||||||
|
from app.log import LOG
|
||||||
from app.models import User, PartnerUser, SyncEvent
|
from app.models import User, PartnerUser, SyncEvent
|
||||||
from app.proton.utils import get_proton_partner
|
from app.proton.utils import get_proton_partner
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -26,18 +30,38 @@ class PostgresDispatcher(Dispatcher):
|
|||||||
return PostgresDispatcher()
|
return PostgresDispatcher()
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalDispatcher:
|
||||||
|
__dispatcher: Optional[Dispatcher] = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dispatcher() -> Dispatcher:
|
||||||
|
if not GlobalDispatcher.__dispatcher:
|
||||||
|
GlobalDispatcher.__dispatcher = PostgresDispatcher.get()
|
||||||
|
return GlobalDispatcher.__dispatcher
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_dispatcher(dispatcher: Optional[Dispatcher]):
|
||||||
|
GlobalDispatcher.__dispatcher = dispatcher
|
||||||
|
|
||||||
|
|
||||||
class EventDispatcher:
|
class EventDispatcher:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def send_event(
|
def send_event(
|
||||||
user: User,
|
user: User,
|
||||||
content: event_pb2.EventContent,
|
content: event_pb2.EventContent,
|
||||||
dispatcher: Dispatcher = PostgresDispatcher.get(),
|
dispatcher: Optional[Dispatcher] = None,
|
||||||
skip_if_webhook_missing: bool = True,
|
skip_if_webhook_missing: bool = True,
|
||||||
):
|
):
|
||||||
|
if dispatcher is None:
|
||||||
|
dispatcher = GlobalDispatcher.get_dispatcher()
|
||||||
if config.EVENT_WEBHOOK_DISABLE:
|
if config.EVENT_WEBHOOK_DISABLE:
|
||||||
|
LOG.i("Not sending events because webhook is disabled")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not config.EVENT_WEBHOOK and skip_if_webhook_missing:
|
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
|
return
|
||||||
|
|
||||||
if config.EVENT_WEBHOOK_ENABLED_USER_IDS is not None:
|
if config.EVENT_WEBHOOK_ENABLED_USER_IDS is not None:
|
||||||
@ -46,6 +70,9 @@ class EventDispatcher:
|
|||||||
|
|
||||||
partner_user = EventDispatcher.__partner_user(user.id)
|
partner_user = EventDispatcher.__partner_user(user.id)
|
||||||
if not partner_user:
|
if not partner_user:
|
||||||
|
LOG.i(
|
||||||
|
f"Not sending events because there's no partner user for user {user}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
event = event_pb2.Event(
|
event = event_pb2.Event(
|
||||||
@ -57,6 +84,8 @@ class EventDispatcher:
|
|||||||
|
|
||||||
serialized = event.SerializeToString()
|
serialized = event.SerializeToString()
|
||||||
dispatcher.send(serialized)
|
dispatcher.send(serialized)
|
||||||
|
newrelic.agent.record_custom_metric("Custom/events_stored", 1)
|
||||||
|
LOG.i("Sent event to the dispatcher")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __partner_user(user_id: int) -> Optional[PartnerUser]:
|
def __partner_user(user_id: int) -> Optional[PartnerUser]:
|
||||||
|
@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12simplelogin_events\"(\n\x0fUserPlanChanged\x12\x15\n\rplan_end_time\x18\x01 \x01(\r\"\r\n\x0bUserDeleted\"Z\n\x0c\x41liasCreated\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\x12\x12\n\nalias_note\x18\x03 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\"L\n\x12\x41liasStatusChanged\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\"5\n\x0c\x41liasDeleted\x12\x10\n\x08\x61lias_id\x18\x01 \x01(\r\x12\x13\n\x0b\x61lias_email\x18\x02 \x01(\t\"D\n\x10\x41liasCreatedList\x12\x30\n\x06\x65vents\x18\x01 \x03(\x0b\x32 .simplelogin_events.AliasCreated\"\x93\x03\n\x0c\x45ventContent\x12?\n\x10user_plan_change\x18\x01 \x01(\x0b\x32#.simplelogin_events.UserPlanChangedH\x00\x12\x37\n\x0cuser_deleted\x18\x02 \x01(\x0b\x32\x1f.simplelogin_events.UserDeletedH\x00\x12\x39\n\ralias_created\x18\x03 \x01(\x0b\x32 .simplelogin_events.AliasCreatedH\x00\x12\x45\n\x13\x61lias_status_change\x18\x04 \x01(\x0b\x32&.simplelogin_events.AliasStatusChangedH\x00\x12\x39\n\ralias_deleted\x18\x05 \x01(\x0b\x32 .simplelogin_events.AliasDeletedH\x00\x12\x41\n\x11\x61lias_create_list\x18\x06 \x01(\x0b\x32$.simplelogin_events.AliasCreatedListH\x00\x42\t\n\x07\x63ontent\"y\n\x05\x45vent\x12\x0f\n\x07user_id\x18\x01 \x01(\r\x12\x18\n\x10\x65xternal_user_id\x18\x02 \x01(\t\x12\x12\n\npartner_id\x18\x03 \x01(\r\x12\x31\n\x07\x63ontent\x18\x04 \x01(\x0b\x32 .simplelogin_events.EventContentb\x06proto3')
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x65vent.proto\x12\x12simplelogin_events\"(\n\x0fUserPlanChanged\x12\x15\n\rplan_end_time\x18\x01 \x01(\r\"\r\n\x0bUserDeleted\"\\\n\x0c\x41liasCreated\x12\n\n\x02id\x18\x01 \x01(\r\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x0c\n\x04note\x18\x03 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x04 \x01(\x08\x12\x12\n\ncreated_at\x18\x05 \x01(\r\"T\n\x12\x41liasStatusChanged\x12\n\n\x02id\x18\x01 \x01(\r\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x12\x12\n\ncreated_at\x18\x04 \x01(\r\")\n\x0c\x41liasDeleted\x12\n\n\x02id\x18\x01 \x01(\r\x12\r\n\x05\x65mail\x18\x02 \x01(\t\"D\n\x10\x41liasCreatedList\x12\x30\n\x06\x65vents\x18\x01 \x03(\x0b\x32 .simplelogin_events.AliasCreated\"\x93\x03\n\x0c\x45ventContent\x12?\n\x10user_plan_change\x18\x01 \x01(\x0b\x32#.simplelogin_events.UserPlanChangedH\x00\x12\x37\n\x0cuser_deleted\x18\x02 \x01(\x0b\x32\x1f.simplelogin_events.UserDeletedH\x00\x12\x39\n\ralias_created\x18\x03 \x01(\x0b\x32 .simplelogin_events.AliasCreatedH\x00\x12\x45\n\x13\x61lias_status_change\x18\x04 \x01(\x0b\x32&.simplelogin_events.AliasStatusChangedH\x00\x12\x39\n\ralias_deleted\x18\x05 \x01(\x0b\x32 .simplelogin_events.AliasDeletedH\x00\x12\x41\n\x11\x61lias_create_list\x18\x06 \x01(\x0b\x32$.simplelogin_events.AliasCreatedListH\x00\x42\t\n\x07\x63ontent\"y\n\x05\x45vent\x12\x0f\n\x07user_id\x18\x01 \x01(\r\x12\x18\n\x10\x65xternal_user_id\x18\x02 \x01(\t\x12\x12\n\npartner_id\x18\x03 \x01(\r\x12\x31\n\x07\x63ontent\x18\x04 \x01(\x0b\x32 .simplelogin_events.EventContentb\x06proto3')
|
||||||
|
|
||||||
_globals = globals()
|
_globals = globals()
|
||||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
@ -36,15 +36,15 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
|||||||
_globals['_USERDELETED']._serialized_start=77
|
_globals['_USERDELETED']._serialized_start=77
|
||||||
_globals['_USERDELETED']._serialized_end=90
|
_globals['_USERDELETED']._serialized_end=90
|
||||||
_globals['_ALIASCREATED']._serialized_start=92
|
_globals['_ALIASCREATED']._serialized_start=92
|
||||||
_globals['_ALIASCREATED']._serialized_end=182
|
_globals['_ALIASCREATED']._serialized_end=184
|
||||||
_globals['_ALIASSTATUSCHANGED']._serialized_start=184
|
_globals['_ALIASSTATUSCHANGED']._serialized_start=186
|
||||||
_globals['_ALIASSTATUSCHANGED']._serialized_end=260
|
_globals['_ALIASSTATUSCHANGED']._serialized_end=270
|
||||||
_globals['_ALIASDELETED']._serialized_start=262
|
_globals['_ALIASDELETED']._serialized_start=272
|
||||||
_globals['_ALIASDELETED']._serialized_end=315
|
_globals['_ALIASDELETED']._serialized_end=313
|
||||||
_globals['_ALIASCREATEDLIST']._serialized_start=317
|
_globals['_ALIASCREATEDLIST']._serialized_start=315
|
||||||
_globals['_ALIASCREATEDLIST']._serialized_end=385
|
_globals['_ALIASCREATEDLIST']._serialized_end=383
|
||||||
_globals['_EVENTCONTENT']._serialized_start=388
|
_globals['_EVENTCONTENT']._serialized_start=386
|
||||||
_globals['_EVENTCONTENT']._serialized_end=791
|
_globals['_EVENTCONTENT']._serialized_end=789
|
||||||
_globals['_EVENT']._serialized_start=793
|
_globals['_EVENT']._serialized_start=791
|
||||||
_globals['_EVENT']._serialized_end=914
|
_globals['_EVENT']._serialized_end=912
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
|
@ -16,34 +16,38 @@ class UserDeleted(_message.Message):
|
|||||||
def __init__(self) -> None: ...
|
def __init__(self) -> None: ...
|
||||||
|
|
||||||
class AliasCreated(_message.Message):
|
class AliasCreated(_message.Message):
|
||||||
__slots__ = ("alias_id", "alias_email", "alias_note", "enabled")
|
__slots__ = ("id", "email", "note", "enabled", "created_at")
|
||||||
ALIAS_ID_FIELD_NUMBER: _ClassVar[int]
|
ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
ALIAS_EMAIL_FIELD_NUMBER: _ClassVar[int]
|
EMAIL_FIELD_NUMBER: _ClassVar[int]
|
||||||
ALIAS_NOTE_FIELD_NUMBER: _ClassVar[int]
|
NOTE_FIELD_NUMBER: _ClassVar[int]
|
||||||
ENABLED_FIELD_NUMBER: _ClassVar[int]
|
ENABLED_FIELD_NUMBER: _ClassVar[int]
|
||||||
alias_id: int
|
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
|
||||||
alias_email: str
|
id: int
|
||||||
alias_note: str
|
email: str
|
||||||
|
note: str
|
||||||
enabled: bool
|
enabled: bool
|
||||||
def __init__(self, alias_id: _Optional[int] = ..., alias_email: _Optional[str] = ..., alias_note: _Optional[str] = ..., enabled: bool = ...) -> None: ...
|
created_at: int
|
||||||
|
def __init__(self, id: _Optional[int] = ..., email: _Optional[str] = ..., note: _Optional[str] = ..., enabled: bool = ..., created_at: _Optional[int] = ...) -> None: ...
|
||||||
|
|
||||||
class AliasStatusChanged(_message.Message):
|
class AliasStatusChanged(_message.Message):
|
||||||
__slots__ = ("alias_id", "alias_email", "enabled")
|
__slots__ = ("id", "email", "enabled", "created_at")
|
||||||
ALIAS_ID_FIELD_NUMBER: _ClassVar[int]
|
ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
ALIAS_EMAIL_FIELD_NUMBER: _ClassVar[int]
|
EMAIL_FIELD_NUMBER: _ClassVar[int]
|
||||||
ENABLED_FIELD_NUMBER: _ClassVar[int]
|
ENABLED_FIELD_NUMBER: _ClassVar[int]
|
||||||
alias_id: int
|
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
|
||||||
alias_email: str
|
id: int
|
||||||
|
email: str
|
||||||
enabled: bool
|
enabled: bool
|
||||||
def __init__(self, alias_id: _Optional[int] = ..., alias_email: _Optional[str] = ..., enabled: bool = ...) -> None: ...
|
created_at: int
|
||||||
|
def __init__(self, id: _Optional[int] = ..., email: _Optional[str] = ..., enabled: bool = ..., created_at: _Optional[int] = ...) -> None: ...
|
||||||
|
|
||||||
class AliasDeleted(_message.Message):
|
class AliasDeleted(_message.Message):
|
||||||
__slots__ = ("alias_id", "alias_email")
|
__slots__ = ("id", "email")
|
||||||
ALIAS_ID_FIELD_NUMBER: _ClassVar[int]
|
ID_FIELD_NUMBER: _ClassVar[int]
|
||||||
ALIAS_EMAIL_FIELD_NUMBER: _ClassVar[int]
|
EMAIL_FIELD_NUMBER: _ClassVar[int]
|
||||||
alias_id: int
|
id: int
|
||||||
alias_email: str
|
email: str
|
||||||
def __init__(self, alias_id: _Optional[int] = ..., alias_email: _Optional[str] = ...) -> None: ...
|
def __init__(self, id: _Optional[int] = ..., email: _Optional[str] = ...) -> None: ...
|
||||||
|
|
||||||
class AliasCreatedList(_message.Message):
|
class AliasCreatedList(_message.Message):
|
||||||
__slots__ = ("events",)
|
__slots__ = ("events",)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import newrelic.agent
|
||||||
|
|
||||||
from app.events.event_dispatcher import EventDispatcher, Dispatcher
|
from app.events.event_dispatcher import EventDispatcher, Dispatcher
|
||||||
from app.events.generated.event_pb2 import EventContent, AliasCreated, AliasCreatedList
|
from app.events.generated.event_pb2 import EventContent, AliasCreated, AliasCreatedList
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
@ -12,6 +14,7 @@ def send_alias_creation_events_for_user(
|
|||||||
return
|
return
|
||||||
chunk_size = min(chunk_size, 50)
|
chunk_size = min(chunk_size, 50)
|
||||||
event_list = []
|
event_list = []
|
||||||
|
LOG.i("Sending alias create events for user {user}")
|
||||||
for alias in (
|
for alias in (
|
||||||
Alias.yield_per_query(chunk_size)
|
Alias.yield_per_query(chunk_size)
|
||||||
.filter_by(user_id=user.id)
|
.filter_by(user_id=user.id)
|
||||||
@ -19,22 +22,31 @@ def send_alias_creation_events_for_user(
|
|||||||
):
|
):
|
||||||
event_list.append(
|
event_list.append(
|
||||||
AliasCreated(
|
AliasCreated(
|
||||||
alias_id=alias.id,
|
id=alias.id,
|
||||||
alias_email=alias.email,
|
email=alias.email,
|
||||||
alias_note=alias.note,
|
note=alias.note,
|
||||||
enabled=alias.enabled,
|
enabled=alias.enabled,
|
||||||
|
created_at=int(alias.created_at.timestamp),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if len(event_list) >= chunk_size:
|
if len(event_list) >= chunk_size:
|
||||||
|
LOG.i(f"Sending {len(event_list)} alias create event for {user}")
|
||||||
EventDispatcher.send_event(
|
EventDispatcher.send_event(
|
||||||
user,
|
user,
|
||||||
EventContent(alias_create_list=AliasCreatedList(events=event_list)),
|
EventContent(alias_create_list=AliasCreatedList(events=event_list)),
|
||||||
dispatcher=dispatcher,
|
dispatcher=dispatcher,
|
||||||
)
|
)
|
||||||
|
newrelic.agent.record_custom_metric(
|
||||||
|
"Custom/event_alias_created_event", len(event_list)
|
||||||
|
)
|
||||||
event_list = []
|
event_list = []
|
||||||
if len(event_list) > 0:
|
if len(event_list) > 0:
|
||||||
|
LOG.i(f"Sending {len(event_list)} alias create event for {user}")
|
||||||
EventDispatcher.send_event(
|
EventDispatcher.send_event(
|
||||||
user,
|
user,
|
||||||
EventContent(alias_create_list=AliasCreatedList(events=event_list)),
|
EventContent(alias_create_list=AliasCreatedList(events=event_list)),
|
||||||
dispatcher=dispatcher,
|
dispatcher=dispatcher,
|
||||||
)
|
)
|
||||||
|
newrelic.agent.record_custom_metric(
|
||||||
|
"Custom/event_alias_created_event", len(event_list)
|
||||||
|
)
|
||||||
|
@ -973,7 +973,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||||||
def has_custom_domain(self):
|
def has_custom_domain(self):
|
||||||
return CustomDomain.filter_by(user_id=self.id, verified=True).count() > 0
|
return CustomDomain.filter_by(user_id=self.id, verified=True).count() > 0
|
||||||
|
|
||||||
def custom_domains(self):
|
def custom_domains(self) -> List["CustomDomain"]:
|
||||||
return CustomDomain.filter_by(user_id=self.id, verified=True).all()
|
return CustomDomain.filter_by(user_id=self.id, verified=True).all()
|
||||||
|
|
||||||
def available_domains_for_random_alias(
|
def available_domains_for_random_alias(
|
||||||
@ -1660,18 +1660,6 @@ class Alias(Base, ModelMixin):
|
|||||||
Session.add(new_alias)
|
Session.add(new_alias)
|
||||||
DailyMetric.get_or_create_today_metric().nb_alias += 1
|
DailyMetric.get_or_create_today_metric().nb_alias += 1
|
||||||
|
|
||||||
# Internal import to avoid global import cycles
|
|
||||||
from app.events.event_dispatcher import EventDispatcher
|
|
||||||
from app.events.generated.event_pb2 import AliasCreated, EventContent
|
|
||||||
|
|
||||||
event = AliasCreated(
|
|
||||||
alias_id=new_alias.id,
|
|
||||||
alias_email=new_alias.email,
|
|
||||||
alias_note=new_alias.note,
|
|
||||||
enabled=True,
|
|
||||||
)
|
|
||||||
EventDispatcher.send_event(user, EventContent(alias_created=event))
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
new_alias.flags & cls.FLAG_PARTNER_CREATED > 0
|
new_alias.flags & cls.FLAG_PARTNER_CREATED > 0
|
||||||
and new_alias.user.flags & User.FLAG_CREATED_ALIAS_FROM_PARTNER == 0
|
and new_alias.user.flags & User.FLAG_CREATED_ALIAS_FROM_PARTNER == 0
|
||||||
@ -1684,6 +1672,19 @@ class Alias(Base, ModelMixin):
|
|||||||
if flush:
|
if flush:
|
||||||
Session.flush()
|
Session.flush()
|
||||||
|
|
||||||
|
# Internal import to avoid global import cycles
|
||||||
|
from app.events.event_dispatcher import EventDispatcher
|
||||||
|
from app.events.generated.event_pb2 import AliasCreated, EventContent
|
||||||
|
|
||||||
|
event = AliasCreated(
|
||||||
|
id=new_alias.id,
|
||||||
|
email=new_alias.email,
|
||||||
|
note=new_alias.note,
|
||||||
|
enabled=True,
|
||||||
|
created_at=int(new_alias.created_at.timestamp),
|
||||||
|
)
|
||||||
|
EventDispatcher.send_event(user, EventContent(alias_created=event))
|
||||||
|
|
||||||
return new_alias
|
return new_alias
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -2418,6 +2419,14 @@ class CustomDomain(Base, ModelMixin):
|
|||||||
sa.Boolean, nullable=False, default=False, server_default="0"
|
sa.Boolean, nullable=False, default=False, server_default="0"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
partner_id = sa.Column(
|
||||||
|
sa.Integer,
|
||||||
|
sa.ForeignKey("partner.id"),
|
||||||
|
nullable=True,
|
||||||
|
default=None,
|
||||||
|
server_default=None,
|
||||||
|
)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index(
|
Index(
|
||||||
"ix_unique_domain", # Index name
|
"ix_unique_domain", # Index name
|
||||||
@ -2442,9 +2451,6 @@ class CustomDomain(Base, ModelMixin):
|
|||||||
def get_trash_url(self):
|
def get_trash_url(self):
|
||||||
return config.URL + f"/dashboard/domains/{self.id}/trash"
|
return config.URL + f"/dashboard/domains/{self.id}/trash"
|
||||||
|
|
||||||
def get_ownership_dns_txt_value(self):
|
|
||||||
return f"sl-verification={self.ownership_txt_token}"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, **kwargs):
|
def create(cls, **kwargs):
|
||||||
domain = kwargs.get("domain")
|
domain = kwargs.get("domain")
|
||||||
|
@ -2,9 +2,11 @@ from dataclasses import dataclass
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
from app import config
|
||||||
from app.errors import LinkException
|
from app.errors import LinkException
|
||||||
from app.models import User, Partner
|
from app.models import User, Partner, Job
|
||||||
from app.proton.proton_client import ProtonClient, ProtonUser
|
from app.proton.proton_client import ProtonClient, ProtonUser
|
||||||
from app.account_linking import (
|
from app.account_linking import (
|
||||||
process_login_case,
|
process_login_case,
|
||||||
@ -41,12 +43,21 @@ class ProtonCallbackHandler:
|
|||||||
def __init__(self, proton_client: ProtonClient):
|
def __init__(self, proton_client: ProtonClient):
|
||||||
self.proton_client = proton_client
|
self.proton_client = proton_client
|
||||||
|
|
||||||
|
def _initial_alias_sync(self, user: User):
|
||||||
|
Job.create(
|
||||||
|
name=config.JOB_SEND_ALIAS_CREATION_EVENTS,
|
||||||
|
payload={"user_id": user.id},
|
||||||
|
run_at=arrow.now(),
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
|
||||||
def handle_login(self, partner: Partner) -> ProtonCallbackResult:
|
def handle_login(self, partner: Partner) -> ProtonCallbackResult:
|
||||||
try:
|
try:
|
||||||
user = self.__get_partner_user()
|
user = self.__get_partner_user()
|
||||||
if user is None:
|
if user is None:
|
||||||
return generate_account_not_allowed_to_log_in()
|
return generate_account_not_allowed_to_log_in()
|
||||||
res = process_login_case(user, partner)
|
res = process_login_case(user, partner)
|
||||||
|
self._initial_alias_sync(res.user)
|
||||||
return ProtonCallbackResult(
|
return ProtonCallbackResult(
|
||||||
redirect_to_login=False,
|
redirect_to_login=False,
|
||||||
flash_message=None,
|
flash_message=None,
|
||||||
@ -75,6 +86,7 @@ class ProtonCallbackHandler:
|
|||||||
if user is None:
|
if user is None:
|
||||||
return generate_account_not_allowed_to_log_in()
|
return generate_account_not_allowed_to_log_in()
|
||||||
res = process_link_case(user, current_user, partner)
|
res = process_link_case(user, current_user, partner)
|
||||||
|
self._initial_alias_sync(res.user)
|
||||||
return ProtonCallbackResult(
|
return ProtonCallbackResult(
|
||||||
redirect_to_login=False,
|
redirect_to_login=False,
|
||||||
flash_message="Account successfully linked",
|
flash_message="Account successfully linked",
|
||||||
|
@ -2,6 +2,7 @@ import requests
|
|||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
from app import config
|
from app import config
|
||||||
|
from app.db import Session
|
||||||
from app.events.event_dispatcher import EventDispatcher
|
from app.events.event_dispatcher import EventDispatcher
|
||||||
from app.events.generated.event_pb2 import EventContent, UserPlanChanged
|
from app.events.generated.event_pb2 import EventContent, UserPlanChanged
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
@ -29,10 +30,11 @@ def execute_subscription_webhook(user: User):
|
|||||||
LOG.i("Sent request to subscription update webhook successfully")
|
LOG.i("Sent request to subscription update webhook successfully")
|
||||||
else:
|
else:
|
||||||
LOG.i(
|
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:
|
except RequestException as e:
|
||||||
LOG.error(f"Subscription request exception: {e}")
|
LOG.error(f"Subscription request exception: {e}")
|
||||||
|
|
||||||
event = UserPlanChanged(plan_end_time=sl_subscription_end)
|
event = UserPlanChanged(plan_end_time=sl_subscription_end)
|
||||||
EventDispatcher.send_event(user, EventContent(user_plan_change=event))
|
EventDispatcher.send_event(user, EventContent(user_plan_change=event))
|
||||||
|
Session.commit()
|
||||||
|
@ -16,12 +16,13 @@ class CannotSetMailbox(Exception):
|
|||||||
|
|
||||||
|
|
||||||
def set_default_alias_domain(user: User, domain_name: Optional[str]):
|
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")
|
LOG.i(f"User {user} has set no domain as default domain")
|
||||||
user.default_alias_public_domain_id = None
|
user.default_alias_public_domain_id = None
|
||||||
user.default_alias_custom_domain_id = None
|
user.default_alias_custom_domain_id = None
|
||||||
Session.flush()
|
Session.flush()
|
||||||
return
|
return
|
||||||
|
|
||||||
sl_domain: SLDomain = SLDomain.get_by(domain=domain_name)
|
sl_domain: SLDomain = SLDomain.get_by(domain=domain_name)
|
||||||
if sl_domain:
|
if sl_domain:
|
||||||
if sl_domain.hidden:
|
if sl_domain.hidden:
|
||||||
|
@ -262,8 +262,6 @@ def get_or_create_contact(from_header: str, mail_from: str, alias: Alias) -> Con
|
|||||||
|
|
||||||
Session.commit()
|
Session.commit()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# If the tx has been rolled back, the connection is borked. Force close to try to get a new one and start fresh
|
|
||||||
Session.close()
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
f"Contact with email {contact_email} for alias_id {alias_id} already existed, fetching from DB"
|
f"Contact with email {contact_email} for alias_id {alias_id} already existed, fetching from DB"
|
||||||
)
|
)
|
||||||
@ -818,7 +816,7 @@ def forward_email_to_mailbox(
|
|||||||
|
|
||||||
email_log = EmailLog.create(
|
email_log = EmailLog.create(
|
||||||
contact_id=contact.id,
|
contact_id=contact.id,
|
||||||
user_id=user.id,
|
user_id=contact.user_id,
|
||||||
mailbox_id=mailbox.id,
|
mailbox_id=mailbox.id,
|
||||||
alias_id=contact.alias_id,
|
alias_id=contact.alias_id,
|
||||||
message_id=str(msg[headers.MESSAGE_ID]),
|
message_id=str(msg[headers.MESSAGE_ID]),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
|
import newrelic.agent
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from app.config import EVENT_WEBHOOK, EVENT_WEBHOOK_SKIP_VERIFY_SSL
|
from app.config import EVENT_WEBHOOK, EVENT_WEBHOOK_SKIP_VERIFY_SSL
|
||||||
@ -26,6 +27,7 @@ class HttpEventSink(EventSink):
|
|||||||
headers={"Content-Type": "application/x-protobuf"},
|
headers={"Content-Type": "application/x-protobuf"},
|
||||||
verify=not EVENT_WEBHOOK_SKIP_VERIFY_SSL,
|
verify=not EVENT_WEBHOOK_SKIP_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
|
newrelic.agent.record_custom_event("event_sent", {"http_code": res.status_code})
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
f"Failed to send event to webhook: {res.status_code} {res.text}"
|
f"Failed to send event to webhook: {res.status_code} {res.text}"
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
"""Custom Domain partner id
|
||||||
|
|
||||||
|
Revision ID: 2441b7ff5da9
|
||||||
|
Revises: 1c14339aae90
|
||||||
|
Create Date: 2024-09-13 15:43:02.425964
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2441b7ff5da9'
|
||||||
|
down_revision = '1c14339aae90'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('custom_domain', sa.Column('partner_id', sa.Integer(), nullable=True, default=None, server_default=None))
|
||||||
|
op.create_foreign_key(None, 'custom_domain', 'partner', ['partner_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'custom_domain', type_='foreignkey')
|
||||||
|
op.drop_column('custom_domain', 'partner_id')
|
||||||
|
# ### end Alembic commands ###
|
49
app/oneshot/alias_partner_set_flag_and_clear_note.py
Normal file
49
app/oneshot/alias_partner_set_flag_and_clear_note.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from app.models import Alias
|
||||||
|
from app.db import Session
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="Backfill alias", description="Update alias notes and backfill flag"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s", "--start_alias_id", default=0, type=int, help="Initial alias_id"
|
||||||
|
)
|
||||||
|
parser.add_argument("-e", "--end_alias_id", default=0, type=int, help="Last alias_id")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
alias_id_start = args.start_alias_id
|
||||||
|
max_alias_id = args.end_alias_id
|
||||||
|
if max_alias_id == 0:
|
||||||
|
max_alias_id = Session.query(func.max(Alias.id)).scalar()
|
||||||
|
|
||||||
|
print(f"Checking alias {alias_id_start} to {max_alias_id}")
|
||||||
|
step = 1000
|
||||||
|
noteSql = "(note = 'Created through Proton' or note = 'Created through partner Proton')"
|
||||||
|
alias_query = f"UPDATE alias set note = NULL, flags = flags | :flag where id>=:start AND id<:end and {noteSql}"
|
||||||
|
updated = 0
|
||||||
|
start_time = time.time()
|
||||||
|
for batch_start in range(alias_id_start, max_alias_id, step):
|
||||||
|
rows_done = Session.execute(
|
||||||
|
alias_query,
|
||||||
|
{
|
||||||
|
"start": batch_start,
|
||||||
|
"end": batch_start + step,
|
||||||
|
"flag": Alias.FLAG_PARTNER_CREATED,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
updated += rows_done.rowcount
|
||||||
|
Session.commit()
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
time_per_alias = elapsed / (updated + 1)
|
||||||
|
last_batch_id = batch_start + step
|
||||||
|
remaining = max_alias_id - last_batch_id
|
||||||
|
time_remaining = (max_alias_id - last_batch_id) * time_per_alias
|
||||||
|
hours_remaining = time_remaining / 3600.0
|
||||||
|
print(
|
||||||
|
f"\rAlias {batch_start}/{max_alias_id} {updated} {hours_remaining:.2f}hrs remaining"
|
||||||
|
)
|
||||||
|
print("")
|
392
app/poetry.lock
generated
392
app/poetry.lock
generated
@ -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]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -276,21 +276,6 @@ files = [
|
|||||||
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
|
{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]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "3.2.0"
|
version = "3.2.0"
|
||||||
@ -491,13 +476,13 @@ pycparser = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfgv"
|
name = "cfgv"
|
||||||
version = "3.2.0"
|
version = "3.4.0"
|
||||||
description = "Validate configuration and produce human readable error messages."
|
description = "Validate configuration and produce human readable error messages."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6.1"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"},
|
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
||||||
{file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"},
|
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -690,6 +675,21 @@ sdist = ["setuptools-rust (>=0.11.4)"]
|
|||||||
ssh = ["bcrypt (>=3.1.5)"]
|
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"]
|
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]]
|
[[package]]
|
||||||
name = "decorator"
|
name = "decorator"
|
||||||
version = "4.4.2"
|
version = "4.4.2"
|
||||||
@ -734,41 +734,40 @@ graph = ["objgraph (>=1.7.2)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "distlib"
|
name = "distlib"
|
||||||
version = "0.3.1"
|
version = "0.3.8"
|
||||||
description = "Distribution utilities"
|
description = "Distribution utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
|
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
|
||||||
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
|
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djlint"
|
name = "djlint"
|
||||||
version = "1.3.0"
|
version = "1.34.1"
|
||||||
description = "HTML Template Linter and Formatter"
|
description = "HTML Template Linter and Formatter"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7,<4.0"
|
python-versions = ">=3.8.0,<4.0.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "djlint-1.3.0-py3-none-any.whl", hash = "sha256:0c986bf542cdac3025d431a5b15e6c3977f652f2e76e408dbb5e7aaab6b73d99"},
|
{file = "djlint-1.34.1-py3-none-any.whl", hash = "sha256:96ff1c464fb6f061130ebc88663a2ea524d7ec51f4b56221a2b3f0320a3cfce8"},
|
||||||
{file = "djlint-1.3.0.tar.gz", hash = "sha256:b2d8e6c0a14f88da165296f0da05795d15299b7ab0a9093d670ce9ffd867bc79"},
|
{file = "djlint-1.34.1.tar.gz", hash = "sha256:db93fa008d19eaadb0454edf1704931d14469d48508daba2df9941111f408346"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
click = ">=8.0.1,<9.0.0"
|
click = ">=8.0.1,<9.0.0"
|
||||||
colorama = ">=0.4.4,<0.5.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-tag-names = ">=0.1.2,<0.2.0"
|
||||||
html-void-elements = ">=0.1.0,<0.2.0"
|
html-void-elements = ">=0.1.0,<0.2.0"
|
||||||
importlib-metadata = ">=4.11.0,<5.0.0"
|
jsbeautifier = ">=1.14.4,<2.0.0"
|
||||||
pathspec = ">=0.9.0,<0.10.0"
|
json5 = ">=0.9.11,<0.10.0"
|
||||||
|
pathspec = ">=0.12.0,<0.13.0"
|
||||||
PyYAML = ">=6.0,<7.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\""}
|
tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
|
||||||
tqdm = ">=4.62.2,<5.0.0"
|
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]]
|
[[package]]
|
||||||
name = "dkimpy"
|
name = "dkimpy"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@ -806,6 +805,16 @@ doh = ["requests", "requests-toolbelt"]
|
|||||||
idna = ["idna (>=2.1)"]
|
idna = ["idna (>=2.1)"]
|
||||||
trio = ["sniffio (>=1.1)", "trio (>=0.14.0)"]
|
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]]
|
[[package]]
|
||||||
name = "email-validator"
|
name = "email-validator"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -851,15 +860,20 @@ requests = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.0.12"
|
version = "3.15.4"
|
||||||
description = "A platform independent file lock."
|
description = "A platform independent file lock."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
|
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
|
||||||
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
|
{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]]
|
[[package]]
|
||||||
name = "flanker"
|
name = "flanker"
|
||||||
version = "0.9.11"
|
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-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
|
||||||
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
|
{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-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-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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
|
||||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
|
{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-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-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_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_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_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"},
|
{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-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
|
||||||
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
|
{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_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-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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
|
{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-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-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
{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-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-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
|
||||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
|
{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]]
|
[[package]]
|
||||||
name = "identify"
|
name = "identify"
|
||||||
version = "1.5.5"
|
version = "2.6.0"
|
||||||
description = "File identification library for Python"
|
description = "File identification library for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "identify-1.5.5-py2.py3-none-any.whl", hash = "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"},
|
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
|
||||||
{file = "identify-1.5.5.tar.gz", hash = "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4"},
|
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
license = ["editdistance"]
|
license = ["ukkonen"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
@ -1518,25 +1528,6 @@ files = [
|
|||||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
{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]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1669,6 +1660,31 @@ files = [
|
|||||||
{file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"},
|
{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]]
|
[[package]]
|
||||||
name = "jwcrypto"
|
name = "jwcrypto"
|
||||||
version = "0.8"
|
version = "0.8"
|
||||||
@ -1959,13 +1975,13 @@ urllib3 = ">=1.7,<2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodeenv"
|
name = "nodeenv"
|
||||||
version = "1.5.0"
|
version = "1.9.1"
|
||||||
description = "Node.js virtual environment builder"
|
description = "Node.js virtual environment builder"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"},
|
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
|
||||||
{file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"},
|
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2015,13 +2031,13 @@ testing = ["docopt", "pytest (>=3.0.7)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "0.9.0"
|
version = "0.12.1"
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2117,13 +2133,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pre-commit"
|
name = "pre-commit"
|
||||||
version = "2.17.0"
|
version = "3.8.0"
|
||||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6.1"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
|
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
||||||
{file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
|
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2131,8 +2147,7 @@ cfgv = ">=2.0.0"
|
|||||||
identify = ">=1.0.0"
|
identify = ">=1.0.0"
|
||||||
nodeenv = ">=0.11.1"
|
nodeenv = ">=0.11.1"
|
||||||
pyyaml = ">=5.1"
|
pyyaml = ">=5.1"
|
||||||
toml = "*"
|
virtualenv = ">=20.10.0"
|
||||||
virtualenv = ">=20.0.8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prompt-toolkit"
|
name = "prompt-toolkit"
|
||||||
@ -2665,85 +2680,104 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "2022.6.2"
|
version = "2023.12.25"
|
||||||
description = "Alternative regular expression module, to replace re."
|
description = "Alternative regular expression module, to replace re."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "regex-2022.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:042d122f9fee3ceb6d7e3067d56557df697d1aad4ff5f64ecce4dc13a90a7c01"},
|
{file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffef4b30785dc2d1604dfb7cf9fca5dc27cd86d65f7c2a9ec34d6d3ae4565ec2"},
|
{file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0afa6a601acf3c0dc6de4e8d7d8bbce4e82f8542df746226cd35d4a6c15e9456"},
|
{file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a11cbe8eb5fb332ae474895b5ead99392a4ea568bd2a258ab8df883e9c2bf92"},
|
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c1f62ee2ba880e221bc950651a1a4b0176083d70a066c83a50ef0cb9b178e12"},
|
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aba3d13c77173e9bfed2c2cea7fc319f11c89a36fcec08755e8fb169cf3b0df"},
|
{file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"},
|
||||||
{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-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"},
|
||||||
{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-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5e201b1232d81ca1a7a22ab2f08e1eccad4e111579fd7f3bbf60b21ef4a16cea"},
|
{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-2022.6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fdecb225d0f1d50d4b26ac423e0032e76d46a788b83b4e299a520717a47d968c"},
|
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:be57f9c7b0b423c66c266a26ad143b2c5514997c05dd32ce7ca95c8b209c2288"},
|
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ed657a07d8a47ef447224ea00478f1c7095065dfe70a89e7280e5f50a5725131"},
|
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24908aefed23dd065b4a668c0b4ca04d56b7f09d8c8e89636cf6c24e64e67a1e"},
|
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-win32.whl", hash = "sha256:775694cd0bb2c4accf2f1cdd007381b33ec8b59842736fe61bdbad45f2ac7427"},
|
{file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"},
|
||||||
{file = "regex-2022.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:809bbbbbcf8258049b031d80932ba71627d2274029386f0452e9950bcfa2c6e8"},
|
{file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2b5d983eb0adf2049d41f95205bdc3de4e6cc2350e9c80d4409d3a75229de"},
|
{file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4c101746a8dac0401abefa716b357c546e61ea2e3d4a564a9db9eac57ccbce"},
|
{file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:166ae7674d0a0e0f8044e7335ba86d0716c9d49465cff1b153f908e0470b8300"},
|
{file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5eac5d8a8ac9ccf00805d02a968a36f5c967db6c7d2b747ab9ed782b3b3a28b"},
|
{file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f57823f35b18d82b201c1b27ce4e55f88e79e81d9ca07b50ce625d33823e1439"},
|
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"},
|
||||||
{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-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"},
|
||||||
{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-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:17764683ea01c2b8f103d99ae9de2473a74340df13ce306c49a721f0b1f0eb9e"},
|
{file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:2ac29b834100d2c171085ceba0d4a1e7046c434ddffc1434dbc7f9d59af1e945"},
|
{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-2022.6.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:f43522fb5d676c99282ca4e2d41e8e2388427c0cf703db6b4a66e49b10b699a8"},
|
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:9faa01818dad9111dbf2af26c6e3c45140ccbd1192c3a0981f196255bf7ec5e6"},
|
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:17443f99b8f255273731f915fdbfea4d78d809bb9c3aaf67b889039825d06515"},
|
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-win32.whl", hash = "sha256:4a5449adef907919d4ce7a1eab2e27d0211d1b255bf0b8f5dd330ad8707e0fc3"},
|
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"},
|
||||||
{file = "regex-2022.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4d206703a96a39763b5b45cf42645776f5553768ea7f3c2c1a39a4f59cafd4ba"},
|
{file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fcd7c432202bcb8b642c3f43d5bcafc5930d82fe5b2bf2c008162df258445c1d"},
|
{file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:186c5a4a4c40621f64d771038ede20fca6c61a9faa8178f9e305aaa0c2442a97"},
|
{file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:047b2d1323a51190c01b6604f49fe09682a5c85d3c1b2c8b67c1cd68419ce3c4"},
|
{file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30637e7fa4acfed444525b1ab9683f714be617862820578c9fd4e944d4d9ad1f"},
|
{file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adafe6f2c6d86dbf3313866b61180530ca4dcd0c264932dc8fa1ffb10871d58"},
|
{file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"},
|
||||||
{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-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"},
|
||||||
{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-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:68e5c641645351eb9eb12c465876e76b53717f99e9b92aea7a2dd645a87aa7aa"},
|
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8fd5f8ae42f789538bb634bdfd69b9aa357e76fdfd7ad720f32f8994c0d84f1e"},
|
{file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:71988a76fcb68cc091e901fddbcac0f9ad9a475da222c47d3cf8db0876cb5344"},
|
{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-2022.6.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:4b8838f70be3ce9e706df9d72f88a0aa7d4c1fea61488e06fdf292ccb70ad2be"},
|
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:663dca677bd3d2e2b5b7d0329e9f24247e6f38f3b740dd9a778a8ef41a76af41"},
|
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-win32.whl", hash = "sha256:24963f0b13cc63db336d8da2a533986419890d128c551baacd934c249d51a779"},
|
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"},
|
||||||
{file = "regex-2022.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ceff75127f828dfe7ceb17b94113ec2df4df274c4cd5533bb299cb099a18a8ca"},
|
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a6f2698cfa8340dfe4c0597782776b393ba2274fe4c079900c7c74f68752705"},
|
{file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8a08ace913c4101f0dc0be605c108a3761842efd5f41a3005565ee5d169fb2b"},
|
{file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26dbe90b724efef7820c3cf4a0e5be7f130149f3d2762782e4e8ac2aea284a0b"},
|
{file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5f759a1726b995dc896e86f17f9c0582b54eb4ead00ed5ef0b5b22260eaf2d0"},
|
{file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fc26bb3415e7aa7495c000a2c13bf08ce037775db98c1a3fac9ff04478b6930"},
|
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52684da32d9003367dc1a1c07e059b9bbaf135ad0764cd47d8ac3dba2df109bc"},
|
{file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"},
|
||||||
{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-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"},
|
||||||
{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-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:27624b490b5d8880f25dac67e1e2ea93dfef5300b98c6755f585799230d6c746"},
|
{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-2022.6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:555f7596fd1f123f8c3a67974c01d6ef80b9769e04d660d6c1a7cc3e6cff7069"},
|
{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-2022.6.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:933e72fbe1829cbd59da2bc51ccd73d73162f087f88521a87a8ec9cb0cf10fa8"},
|
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:cff5c87e941292c97d11dc81bd20679f56a2830f0f0e32f75b8ed6e0eb40f704"},
|
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c757f3a27b6345de13ef3ca956aa805d7734ce68023e84d0fc74e1f09ce66f7a"},
|
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-win32.whl", hash = "sha256:a58d21dd1a2d6b50ed091554ff85e448fce3fe33a4db8b55d0eba2ca957ed626"},
|
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"},
|
||||||
{file = "regex-2022.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:495a4165172848503303ed05c9d0409428f789acc27050fe2cf0a4549188a7d5"},
|
{file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1ab5cf7d09515548044e69d3a0ec77c63d7b9dfff4afc19653f638b992573126"},
|
{file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1ea28f0ee6cbe4c0367c939b015d915aa9875f6e061ba1cf0796ca9a3010570"},
|
{file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de1ecf26ce85521bf73897828b6d0687cc6cf271fb6ff32ac63d26b21f5e764"},
|
{file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7c7044aabdad2329974be2246babcc21d3ede852b3971a90fd8c2056c20360"},
|
{file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53d69d77e9cfe468b000314dd656be85bb9e96de088a64f75fe128dfe1bf30dd"},
|
{file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c8d61883a38b1289fba9944a19a361875b5c0170b83cdcc95ea180247c1b7d3"},
|
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"},
|
||||||
{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-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"},
|
||||||
{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-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c400dfed4137f32127ea4063447006d7153c974c680bf0fb1b724cce9f8567fc"},
|
{file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f648037c503985aed39f85088acab6f1eb6a0482d7c6c665a5712c9ad9eaefc"},
|
{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-2022.6.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e7b2ff451f6c305b516281ec45425dd423223c8063218c5310d6f72a0a7a517c"},
|
{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-2022.6.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:be456b4313a86be41706319c397c09d9fdd2e5cdfde208292a277b867e99e3d1"},
|
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c3db393b21b53d7e1d3f881b64c29d886cbfdd3df007e31de68b329edbab7d02"},
|
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-win32.whl", hash = "sha256:d70596f20a03cb5f935d6e4aad9170a490d88fc4633679bf00c652e9def4619e"},
|
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"},
|
||||||
{file = "regex-2022.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:3b9b6289e03dbe6a6096880d8ac166cb23c38b4896ad235edee789d4e8697152"},
|
{file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"},
|
||||||
{file = "regex-2022.6.2.tar.gz", hash = "sha256:f7b43acb2c46fb2cd506965b2d9cf4c5e64c9c612bac26c1187933c7296bf08c"},
|
{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]]
|
[[package]]
|
||||||
@ -3130,17 +3164,6 @@ idna = "*"
|
|||||||
requests = ">=2.1.0"
|
requests = ">=2.1.0"
|
||||||
requests-file = ">=1.4"
|
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]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@ -3288,25 +3311,23 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "20.8.1"
|
version = "20.21.1"
|
||||||
description = "Virtual Python Environment builder"
|
description = "Virtual Python Environment builder"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"},
|
{file = "virtualenv-20.21.1-py3-none-any.whl", hash = "sha256:09ddbe1af0c8ed2bb4d6ed226b9e6415718ad18aef9fa0ba023d96b7a8356049"},
|
||||||
{file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"},
|
{file = "virtualenv-20.21.1.tar.gz", hash = "sha256:4c104ccde994f8b108163cf9ba58f3d11511d9403de87fb9b4f52bf33dbc8668"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
"backports.entry-points-selectable" = ">=1.0.4"
|
distlib = ">=0.3.6,<1"
|
||||||
distlib = ">=0.3.1,<1"
|
filelock = ">=3.4.1,<4"
|
||||||
filelock = ">=3.0.0,<4"
|
platformdirs = ">=2.4,<4"
|
||||||
platformdirs = ">=2,<3"
|
|
||||||
six = ">=1.9.0,<2"
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
|
docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
|
||||||
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)"]
|
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]]
|
[[package]]
|
||||||
name = "watchtower"
|
name = "watchtower"
|
||||||
@ -3605,21 +3626,6 @@ files = [
|
|||||||
idna = ">=2.0"
|
idna = ">=2.0"
|
||||||
multidict = ">=4.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]]
|
[[package]]
|
||||||
name = "zope.event"
|
name = "zope.event"
|
||||||
version = "4.5.0"
|
version = "4.5.0"
|
||||||
@ -3698,4 +3704,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "01afc410d21eeac0a0ac7e8ef6eeb0a991cf4bc091c3351049263462e205ff63"
|
content-hash = "22b9a61e9999a215aacb889b3790ee1a6840ce249aea2e3d16c6113243d5c126"
|
||||||
|
@ -10,21 +10,23 @@ message UserDeleted {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message AliasCreated {
|
message AliasCreated {
|
||||||
uint32 alias_id = 1;
|
uint32 id = 1;
|
||||||
string alias_email = 2;
|
string email = 2;
|
||||||
string alias_note = 3;
|
string note = 3;
|
||||||
bool enabled = 4;
|
bool enabled = 4;
|
||||||
|
uint32 created_at = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AliasStatusChanged {
|
message AliasStatusChanged {
|
||||||
uint32 alias_id = 1;
|
uint32 id = 1;
|
||||||
string alias_email = 2;
|
string email = 2;
|
||||||
bool enabled = 3;
|
bool enabled = 3;
|
||||||
|
uint32 created_at = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AliasDeleted {
|
message AliasDeleted {
|
||||||
uint32 alias_id = 1;
|
uint32 id = 1;
|
||||||
string alias_email = 2;
|
string email = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AliasCreatedList {
|
message AliasCreatedList {
|
||||||
|
@ -121,13 +121,13 @@ aiospamc = "0.10"
|
|||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^7.0.0"
|
pytest = "^7.0.0"
|
||||||
pytest-cov = "^3.0.0"
|
pytest-cov = "^3.0.0"
|
||||||
pre-commit = "^2.17.0"
|
|
||||||
black = "^22.1.0"
|
black = "^22.1.0"
|
||||||
djlint = "^1.3.0"
|
djlint = "^1.3.0"
|
||||||
pylint = "^2.14.4"
|
pylint = "^2.14.4"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ruff = "^0.1.5"
|
ruff = "^0.1.5"
|
||||||
|
pre-commit = "^3.8.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
|
@ -12,10 +12,10 @@ docker run -p 25432:5432 --name ${container_name} -e POSTGRES_PASSWORD=postgres
|
|||||||
sleep 3
|
sleep 3
|
||||||
|
|
||||||
# upgrade the DB to the latest stage and
|
# upgrade the DB to the latest stage and
|
||||||
env DB_URI=postgresql://postgres:postgres@127.0.0.1:25432/sl rye run alembic upgrade head
|
env DB_URI=postgresql://postgres:postgres@127.0.0.1:25432/sl poetry run alembic upgrade head
|
||||||
|
|
||||||
# generate the migration script.
|
# generate the migration script.
|
||||||
env DB_URI=postgresql://postgres:postgres@127.0.0.1:25432/sl rye run alembic revision --autogenerate $@
|
env DB_URI=postgresql://postgres:postgres@127.0.0.1:25432/sl poetry run alembic revision --autogenerate $@
|
||||||
|
|
||||||
# remove the db
|
# remove the db
|
||||||
docker rm -f ${container_name}
|
docker rm -f ${container_name}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">{{ field.label }}</label>
|
<label class="col-sm-2 col-form-label">{{ field.label }}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
{{ field(**kwargs)|safe }}
|
{{ field(**kwargs) |safe }}
|
||||||
<small class="form-text text-muted">{{ field.description }}</small>
|
<small class="form-text text-muted">{{ field.description }}</small>
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
|
|
||||||
|
@ -2,31 +2,48 @@
|
|||||||
|
|
||||||
{% macro show_user(user) -%}
|
{% macro show_user(user) -%}
|
||||||
<h4>User {{ user.email }} with ID {{ user.id }}.</h4>
|
<h4>User {{ user.email }} with ID {{ user.id }}.</h4>
|
||||||
|
{% set pu = helper.partner_user(user) %}
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">User ID</th>
|
<th scope="col">User ID</th>
|
||||||
<th scope="col">Email</th>
|
<th scope="col">Email</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
<th scope="col">Paid</th>
|
<th scope="col">Paid</th>
|
||||||
<th>Subscription</th>
|
<th>Subscription</th>
|
||||||
<th>Created At</th>
|
<th>Created At</th>
|
||||||
|
<th>Updated At</th>
|
||||||
|
<th>Connected with Proton account</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.id }}</td>
|
<td>{{ user.id }}</td>
|
||||||
<td>{{ user.email }}</td>
|
<td><a href="?email={{ user.email }}">{{ user.email }}</a></td>
|
||||||
<td>{{ "yes" if user.is_paid() else No }}</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.get_active_subscription() }}</td>
|
||||||
<td>{{ user.created_at }}</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
{% macro list_mailboxes(mbox_count, mboxes) %}
|
{% macro list_mailboxes(mbox_count, mboxes) %}
|
||||||
<h4>
|
<h4>
|
||||||
{{ mbox_count }} Mailboxes found.
|
{{ 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>
|
</h4>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -42,9 +59,11 @@
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ mailbox.id }}</td>
|
<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>{{ "Yes" if mailbox.verified else "No" }}</td>
|
||||||
<td>{{ mailbox.created_at }}</td>
|
<td>
|
||||||
|
{{ mailbox.created_at }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -53,67 +72,54 @@
|
|||||||
{% macro list_alias(alias_count, aliases) %}
|
{% macro list_alias(alias_count, aliases) %}
|
||||||
<h4>
|
<h4>
|
||||||
{{ alias_count }} Aliases found.
|
{{ 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>
|
</h4>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Alias ID</th>
|
<th>
|
||||||
<th>Email</th>
|
Alias ID
|
||||||
<th>Verified</th>
|
</th>
|
||||||
<th>Created At</th>
|
<th>
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Verified
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Created At
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for alias in aliases %}
|
{% for alias in aliases %}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ alias.id }}</td>
|
<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>{{ "Yes" if alias.verified else "No" }}</td>
|
||||||
<td>
|
<td>{{ alias.created_at }}</td>
|
||||||
{{ alias.created_at }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{% macro show_deleted_alias(deleted_alias) -%}
|
{% macro show_deleted_alias(deleted_alias) -%}
|
||||||
<h4>
|
<h4>Deleted Alias {{ deleted_alias.email }} with ID {{ deleted_alias.id }}.</h4>
|
||||||
Deleted Alias {{ deleted_alias.email }} with ID {{ deleted_alias.id }}.
|
|
||||||
</h4>
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">Deleted Alias ID</th>
|
||||||
Deleted Alias ID
|
<th scope="col">Email</th>
|
||||||
</th>
|
<th scope="col">Deleted At</th>
|
||||||
<th scope="col">
|
<th scope="col">Reason</th>
|
||||||
Email
|
|
||||||
</th>
|
|
||||||
<th scope="col">
|
|
||||||
Deleted At
|
|
||||||
</th>
|
|
||||||
<th scope="col">
|
|
||||||
Reason
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>{{ deleted_alias.id }}</td>
|
||||||
{{ deleted_alias.id }}
|
<td>{{ deleted_alias.email }}</td>
|
||||||
</td>
|
<td>{{ deleted_alias.created_at }}</td>
|
||||||
<td>
|
<td>{{ deleted_alias.reason }}</td>
|
||||||
{{ deleted_alias.email }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ deleted_alias.created_at }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{{ deleted_alias.reason }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -125,49 +131,23 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">Deleted Alias ID</th>
|
||||||
Deleted Alias ID
|
<th scope="col">Email</th>
|
||||||
</th>
|
<th scope="col">Domain</th>
|
||||||
<th scope="col">
|
<th scope="col">Domain ID</th>
|
||||||
Email
|
<th scope="col">Domain owner user ID</th>
|
||||||
</th>
|
<th scope="col">Domain owner user email</th>
|
||||||
<th scope="col">
|
<th scope="col">Deleted At</th>
|
||||||
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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>{{ dom_deleted_alias.id }}</td>
|
||||||
{{ dom_deleted_alias.id }}
|
<td>{{ dom_deleted_alias.email }}</td>
|
||||||
</td>
|
<td>{{ dom_deleted_alias.domain.domain }}</td>
|
||||||
<td>
|
<td>{{ dom_deleted_alias.domain.id }}</td>
|
||||||
{{ dom_deleted_alias.email }}
|
<td>{{ dom_deleted_alias.domain.user_id }}</td>
|
||||||
</td>
|
<td>{{ dom_deleted_alias.created_at }}</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -176,75 +156,64 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||||
<form method="post">
|
<form method="get">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">
|
<label for="email">Email to search:</label>
|
||||||
Email to search:
|
|
||||||
</label>
|
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="email"
|
name="email"
|
||||||
value="{{ email or '' }}"/>
|
value="{{ email or '' }}" />
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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"
|
<div class="border border-dark border-2 mt-1 mb-2 p-3 alert alert-warning"
|
||||||
role="alert">
|
role="alert">No user, alias or mailbox found for {{ email }}</div>
|
||||||
No user, alias or mailbox found for {{ email }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data.alias %}
|
{% if data.alias %}
|
||||||
|
|
||||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">Found Alias {{ data.alias.email }}</h3>
|
||||||
Found Alias {{ data.alias.email }}
|
|
||||||
</h3>
|
|
||||||
{{ list_alias(1,[data.alias]) }}
|
{{ list_alias(1,[data.alias]) }}
|
||||||
{{ show_user(data.alias.user) }}
|
{{ show_user(data.alias.user) }}
|
||||||
{{ list_mailboxes(helper.mailbox_count(data.alias.user), helper.mailbox_list(data.alias.user) ) }}
|
{{ list_mailboxes(helper.mailbox_count(data.alias.user) , helper.mailbox_list(data.alias.user) ) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data.user %}
|
{% if data.user %}
|
||||||
|
|
||||||
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
<div class="border border-dark border-2 mt-1 mb-2 p-3">
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">Found User {{ data.user.email }}</h3>
|
||||||
Found User {{ data.user.email }}
|
|
||||||
</h3>
|
|
||||||
{{ show_user(data.user) }}
|
{{ show_user(data.user) }}
|
||||||
{{ list_mailboxes(helper.mailbox_count(data.user), helper.mailbox_list(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)) }}
|
{{ list_alias(helper.alias_count(data.user) ,helper.alias_list(data.user)) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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">
|
<div class="border border-dark mt-1 mb-2 p-3">
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">Found Mailbox {{ mailbox.email }}</h3>
|
||||||
Found Mailbox {{ data.mailbox.email }}
|
{{ list_mailboxes(1, [mailbox]) }}
|
||||||
</h3>
|
{{ show_user(mailbox.user) }}
|
||||||
{{ list_mailboxes(1, [data.mailbox] ) }}
|
|
||||||
{{ show_user(data.mailbox.user) }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{% if data.deleted_alias %}
|
{% if data.deleted_alias %}
|
||||||
|
|
||||||
<div class="border border-dark mt-1 mb-2 p-3">
|
<div class="border border-dark mt-1 mb-2 p-3">
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">Found DeletedAlias {{ data.deleted_alias.email }}</h3>
|
||||||
Found DeletedAlias {{ data.deleted_alias.email }}
|
|
||||||
</h3>
|
|
||||||
{{ show_deleted_alias(data.deleted_alias) }}
|
{{ show_deleted_alias(data.deleted_alias) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data.domain_deleted_alias %}
|
{% if data.domain_deleted_alias %}
|
||||||
|
|
||||||
<div class="border border-dark mt-1 mb-2 p-3">
|
<div class="border border-dark mt-1 mb-2 p-3">
|
||||||
<h3 class="mb-3">
|
<h3 class="mb-3">Found DomainDeletedAlias {{ data.domain_deleted_alias.email }}</h3>
|
||||||
Found DomainDeletedAlias {{ data.domain_deleted_alias.email }}
|
|
||||||
</h3>
|
|
||||||
{{ show_domain_deleted_alias(data.domain_deleted_alias) }}
|
{{ show_domain_deleted_alias(data.domain_deleted_alias) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -11,11 +11,11 @@ Based on https://github.com/flask-admin/flask-admin/issues/974#issuecomment-1682
|
|||||||
<input name="user_id"
|
<input name="user_id"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="User ID"
|
placeholder="User ID"
|
||||||
aria-describedby="userID"/>
|
aria-describedby="userID" />
|
||||||
<input name="to_address"
|
<input name="to_address"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Specify an address to receive the newsletter for testing"
|
placeholder="Specify an address to receive the newsletter for testing"
|
||||||
aria-describedby="Email address"/>
|
aria-describedby="Email address" />
|
||||||
</li>
|
</li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block tail %}
|
{% block tail %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="text-center text-muted small mt-4">
|
<div class="text-center text-muted small mt-4">
|
||||||
Ask for another activation email?
|
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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
Please go to
|
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.
|
page to re-send the confirmation email.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
<div class="text-muted mt-5" style="margin-top: 1em;">
|
<div class="text-muted mt-5" style="margin-top: 1em;">
|
||||||
Don't have your key with you?
|
Don't have your key with you?
|
||||||
<br />
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="text-center text-muted">
|
<div class="text-center text-muted">
|
||||||
Forget it,
|
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.
|
to the sign in screen.
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="text-center text-muted small mb-4">
|
<div class="text-center text-muted small mb-4">
|
||||||
You haven't received the activation email?
|
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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card" style="border-radius: 2%">
|
<div class="card" style="border-radius: 2%">
|
||||||
@ -25,7 +25,7 @@
|
|||||||
{{ form.password(class="form-control", type="password") }}
|
{{ form.password(class="form-control", type="password") }}
|
||||||
{{ render_field_errors(form.password) }}
|
{{ render_field_errors(form.password) }}
|
||||||
<div class="text-muted">
|
<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>
|
</div>
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
@ -57,6 +57,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-center text-muted mt-2">
|
<div class="text-center text-muted mt-2">
|
||||||
Don't have an account yet?
|
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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<div class="text-muted mt-5" style="margin-top: 1em;">
|
<div class="text-muted mt-5" style="margin-top: 1em;">
|
||||||
Having trouble with your authenticator?
|
Having trouble with your authenticator?
|
||||||
<br />
|
<br />
|
||||||
<a href="{{ url_for('auth.fido') }}">
|
<a href="{{ url_for("auth.fido") }}">
|
||||||
Verify by your security
|
Verify by your security
|
||||||
key
|
key
|
||||||
</a>
|
</a>
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<!-- TODO: add terms
|
<!-- TODO: add terms
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="custom-control custom-checkbox">
|
<label class="custom-control custom-checkbox">
|
||||||
<input type="checkbox" class="custom-control-input"/>
|
<input type="checkbox" class="custom-control-input" />
|
||||||
<span class="custom-control-label">Agree the <a href="terms.html">terms and policy</a></span>
|
<span class="custom-control-label">Agree the <a href="terms.html">terms and policy</a></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -69,6 +69,6 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="text-center text-muted mb-6">
|
<div class="text-center text-muted mb-6">
|
||||||
Already have account?
|
Already have account?
|
||||||
<a href="{{ url_for('auth.login') }}">Sign in</a>
|
<a href="{{ url_for("auth.login") }}">Sign in</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -19,6 +19,6 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="text-center text-muted">
|
<div class="text-center text-muted">
|
||||||
Don't have account yet?
|
Don't have account yet?
|
||||||
<a href="{{ url_for('auth.register') }}">Sign up</a>
|
<a href="{{ url_for("auth.register") }}">Sign up</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -29,7 +29,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center p-3"
|
<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>
|
<span class="badge badge-warning">Warning</span>
|
||||||
Please note that social login is now <b>deprecated</b>.
|
Please note that social login is now <b>deprecated</b>.
|
||||||
<br />
|
<br />
|
||||||
@ -39,8 +41,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-muted mt-2">
|
<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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
{% from "_formhelpers.html" import render_field, render_field_errors %}
|
{% from "_formhelpers.html" import render_field, render_field_errors %}
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en"
|
<html lang="en"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
data-theme="{%- if request.cookies.get('dark-mode') == 'true' -%} dark{%- endif -%}">
|
data-theme="{%- if request.cookies.get('dark-mode') == 'true' -%} dark{%- endif -%}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport"
|
<meta name="viewport"
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"/>
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<meta http-equiv="Content-Language" content="en" />
|
<meta http-equiv="Content-Language" content="en" />
|
||||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||||
<meta name="theme-color" content="#4188c9" />
|
<meta name="theme-color" content="#4188c9" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style"
|
<meta name="apple-mobile-web-app-status-bar-style"
|
||||||
content="black-translucent"/>
|
content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="HandheldFriendly" content="True" />
|
<meta name="HandheldFriendly" content="True" />
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<!-- Yandex -->
|
<!-- Yandex -->
|
||||||
<meta name="yandex-verification" content="c9e5d4d68bc983a1" />
|
<meta name="yandex-verification" content="c9e5d4d68bc983a1" />
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="Protect your email address with email ALIAS. Create a different email alias for each website. No more phishing, or spam."/>
|
content="Protect your email address with email ALIAS. Create a different email alias for each website. No more phishing, or spam." />
|
||||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/static/favicon.ico" type="image/x-icon" />
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico" />
|
<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico" />
|
||||||
<link rel="canonical" href="{{ CANONICAL_URL }}" />
|
<link rel="canonical" href="{{ CANONICAL_URL }}" />
|
||||||
@ -32,7 +32,7 @@
|
|||||||
| SimpleLogin
|
| SimpleLogin
|
||||||
</title>
|
</title>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ url_for('static', filename='node_modules/font-awesome/css/font-awesome.css') }}"/>
|
href="{{ url_for('static', filename='node_modules/font-awesome/css/font-awesome.css') }}" />
|
||||||
<!-- Dashboard Core -->
|
<!-- Dashboard Core -->
|
||||||
<link href="/static/assets/css/dashboard.css" rel="stylesheet" />
|
<link href="/static/assets/css/dashboard.css" rel="stylesheet" />
|
||||||
<!-- Tabler JS -->
|
<!-- Tabler JS -->
|
||||||
@ -51,19 +51,19 @@
|
|||||||
<!-- IntroJS -->
|
<!-- IntroJS -->
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
href="{{ url_for('static', filename='node_modules/intro.js/minified/introjs.min.css') }}"/>
|
href="{{ url_for('static', filename='node_modules/intro.js/minified/introjs.min.css') }}" />
|
||||||
<script src="{{ url_for('static', filename='node_modules/intro.js/minified/intro.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='node_modules/intro.js/minified/intro.min.js') }}"></script>
|
||||||
<!-- Sentry -->
|
<!-- Sentry -->
|
||||||
<script src="{{ url_for('static', filename='node_modules/@sentry/browser/build/bundle.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='node_modules/@sentry/browser/build/bundle.min.js') }}"></script>
|
||||||
<link rel="stylesheet" href="/static/vendor/bootstrap-social.min.css" />
|
<link rel="stylesheet" href="/static/vendor/bootstrap-social.min.css" />
|
||||||
<!-- Toastr library -->
|
<!-- Toastr library -->
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ url_for('static', filename='node_modules/toastr/build/toastr.min.css') }}"/>
|
href="{{ url_for('static', filename='node_modules/toastr/build/toastr.min.css') }}" />
|
||||||
<script src="{{ url_for('static', filename='node_modules/toastr/build/toastr.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='node_modules/toastr/build/toastr.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='node_modules/bootbox/dist/bootbox.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='node_modules/bootbox/dist/bootbox.min.js') }}"></script>
|
||||||
<!-- Multiple-select library -->
|
<!-- Multiple-select library -->
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ url_for('static', filename='node_modules/multiple-select/dist/multiple-select.min.css') }}"/>
|
href="{{ url_for('static', filename='node_modules/multiple-select/dist/multiple-select.min.css') }}" />
|
||||||
<script src="{{ url_for('static', filename='node_modules/multiple-select/dist/multiple-select.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='node_modules/multiple-select/dist/multiple-select.min.js') }}"></script>
|
||||||
<!-- Parseley library -->
|
<!-- Parseley library -->
|
||||||
<script src="{{ url_for('static', filename='node_modules/parsleyjs/dist/parsley.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='node_modules/parsleyjs/dist/parsley.min.js') }}"></script>
|
||||||
@ -75,10 +75,10 @@
|
|||||||
<script async defer data-domain=”{{ PLAUSIBLE_DOMAIN }}” src=”{{ PLAUSIBLE_HOST }}/js/plausible.outbound-links.js”></script>
|
<script async defer data-domain=”{{ PLAUSIBLE_DOMAIN }}” src=”{{ PLAUSIBLE_HOST }}/js/plausible.outbound-links.js”></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ url_for('static', filename='darkmode.css') }}?v={{ VERSION }}"/>
|
href="{{ url_for('static', filename='darkmode.css') }}?v={{ VERSION }}" />
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
type="text/css"
|
type="text/css"
|
||||||
href="/static/style.css?v={{ VERSION }}"/>
|
href="/static/style.css?v={{ VERSION }}" />
|
||||||
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
||||||
<script>toastr.options.closeButton = true;</script>
|
<script>toastr.options.closeButton = true;</script>
|
||||||
<!-- For additional head -->
|
<!-- For additional head -->
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
This email address is used to log in to SimpleLogin.
|
This email address is used to log in to SimpleLogin.
|
||||||
<br />
|
<br />
|
||||||
If you want to change the mailbox that emails are forwarded to, use the
|
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
|
<i class="fe fe-inbox"></i> Mailboxes page
|
||||||
</a>
|
</a>
|
||||||
instead.
|
instead.
|
||||||
@ -50,14 +50,14 @@
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-danger float-left">Pending email change: {{ pending_email }}</span>
|
<span class="text-danger float-left">Pending email change: {{ pending_email }}</span>
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{{ url_for('dashboard.resend_email_change') }}"
|
action="{{ url_for("dashboard.resend_email_change") }}"
|
||||||
class="float-left ml-2">
|
class="float-left ml-2">
|
||||||
{{ change_email_form.csrf_token }}
|
{{ change_email_form.csrf_token }}
|
||||||
<a onclick="this.closest('form').submit()"
|
<a onclick="this.closest('form').submit()"
|
||||||
class="btn btn-secondary btn-sm">Resend confirmation email</a>
|
class="btn btn-secondary btn-sm">Resend confirmation email</a>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{{ url_for('dashboard.cancel_email_change') }}"
|
action="{{ url_for("dashboard.cancel_email_change") }}"
|
||||||
class="float-left ml-2">
|
class="float-left ml-2">
|
||||||
{{ change_email_form.csrf_token }}
|
{{ change_email_form.csrf_token }}
|
||||||
<a onclick="this.closest('form').submit()"
|
<a onclick="this.closest('form').submit()"
|
||||||
@ -91,10 +91,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if not current_user.enable_otp %}
|
{% 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>
|
class="btn btn-outline-primary">Setup TOTP</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('dashboard.mfa_cancel') }}"
|
<a href="{{ url_for("dashboard.mfa_cancel") }}"
|
||||||
class="btn btn-outline-danger">Disable TOTP</a>
|
class="btn btn-outline-danger">Disable TOTP</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -111,10 +111,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if current_user.fido_uuid is none %}
|
{% 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>
|
class="btn btn-outline-primary">Setup WebAuthn</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('dashboard.fido_manage') }}"
|
<a href="{{ url_for("dashboard.fido_manage") }}"
|
||||||
class="btn btn-outline-info">Manage WebAuthn</a>
|
class="btn btn-outline-info">Manage WebAuthn</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +146,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title">Account Deletion</div>
|
<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>
|
<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>
|
class="btn btn-outline-danger">Delete account</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<img src="/static/images/reverse-alias.svg"
|
<img src="/static/images/reverse-alias.svg"
|
||||||
style="border: 1px solid"
|
style="border: 1px solid"
|
||||||
class="my-2 img-fluid"/>
|
class="my-2 img-fluid" />
|
||||||
</p>
|
</p>
|
||||||
<p>This might seem like "magic" but trust us, only the first time is a bit awkward.</p>
|
<p>This might seem like "magic" but trust us, only the first time is a bit awkward.</p>
|
||||||
<p>
|
<p>
|
||||||
@ -75,9 +75,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<button disabled
|
<button disabled
|
||||||
title="Upgrade to premium to create reverse-aliases"
|
title="Upgrade to premium to create reverse-aliases"
|
||||||
class="btn btn-primary mt-2">
|
class="btn btn-primary mt-2">Create reverse-alias</button>
|
||||||
Create reverse-alias
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -98,9 +96,7 @@
|
|||||||
{% if highlight_contact_id %}
|
{% if highlight_contact_id %}
|
||||||
|
|
||||||
<a href="{{ url_for("dashboard.alias_contact_manager", alias_id=alias.id, highlight_contact_id=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">
|
class="btn btn-light">Reset</a>
|
||||||
Reset
|
|
||||||
</a>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for("dashboard.alias_contact_manager", alias_id=alias.id) }}"
|
<a href="{{ url_for("dashboard.alias_contact_manager", alias_id=alias.id) }}"
|
||||||
class="btn btn-light">Reset</a>
|
class="btn btn-light">Reset</a>
|
||||||
@ -114,7 +110,7 @@
|
|||||||
|
|
||||||
{% set contact = contact_info.contact %}
|
{% set contact = contact_info.contact %}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="my-2 p-2 card {% if contact.id == highlight_contact_id %} highlight-row{% endif %}">
|
<div class="my-2 p-2 card {% if contact.id == highlight_contact_id %}highlight-row{% endif %}">
|
||||||
<div class="mb-2 row">
|
<div class="mb-2 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<span class="font-weight-bold">{{ contact.website_email }}</span>
|
<span class="font-weight-bold">{{ contact.website_email }}</span>
|
||||||
@ -139,15 +135,11 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="You can click on this to open your email client. Or use the copy button 👉"
|
title="You can click on this to open your email client. Or use the copy button 👉"
|
||||||
class="font-weight-bold">
|
class="font-weight-bold">*************************</a>
|
||||||
*************************
|
|
||||||
</a>
|
|
||||||
<span class="clipboard btn btn-sm btn-success copy-btn"
|
<span class="clipboard btn btn-sm btn-success copy-btn"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="Copy the reverse-alias to clipboard"
|
title="Copy the reverse-alias to clipboard"
|
||||||
data-clipboard-text="{{ contact.website_send_to() }}">
|
data-clipboard-text="{{ contact.website_send_to() }}">Copy reverse-alias</span>
|
||||||
Copy reverse-alias
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 text-muted small-text">
|
<div class="mb-2 text-muted small-text">
|
||||||
@ -207,14 +199,12 @@
|
|||||||
<nav aria-label="Contact navigation">
|
<nav aria-label="Contact navigation">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="btn btn-outline-secondary {% if page == 0 %}disabled{% endif %}"
|
<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) }}">
|
||||||
href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page-1) }}">
|
|
||||||
Previous
|
Previous
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
|
<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) }}">
|
||||||
href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page+1) }}">
|
|
||||||
Next
|
Next
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Total</div>
|
<div class="subheader">Total</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ total }}</div>
|
<div class="h1 m-0">{{ total }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -25,7 +27,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Forwarded</div>
|
<div class="subheader">Forwarded</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ email_forwarded }}</div>
|
<div class="h1 m-0">{{ email_forwarded }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,7 +41,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Replies/Sent</div>
|
<div class="subheader">Replies/Sent</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ email_replied }}</div>
|
<div class="h1 m-0">{{ email_replied }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,7 +55,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Blocked</div>
|
<div class="subheader">Blocked</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ email_blocked }}</div>
|
<div class="h1 m-0">{{ email_blocked }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -111,14 +119,12 @@
|
|||||||
<nav aria-label="Alias log navigation">
|
<nav aria-label="Alias log navigation">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="btn btn-outline-secondary {% if page_id == 0 %}disabled{% endif %}"
|
<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) }}">
|
||||||
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id-1) }}">
|
|
||||||
Previous
|
Previous
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
|
<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) }}">
|
||||||
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id+1) }}">
|
|
||||||
Next
|
Next
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -15,10 +15,7 @@
|
|||||||
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
|
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
|
||||||
{% for mailbox in mailboxes %}
|
{% for mailbox in mailboxes %}
|
||||||
|
|
||||||
<option value="{{ mailbox.id }}"
|
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
|
||||||
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
|
|
||||||
{{ mailbox.email }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-success mt-2">Confirm</button>
|
<button class="btn btn-success mt-2">Confirm</button>
|
||||||
|
@ -16,9 +16,7 @@
|
|||||||
<em data-toggle="tooltip"
|
<em data-toggle="tooltip"
|
||||||
title="Click to copy"
|
title="Click to copy"
|
||||||
class="clipboard"
|
class="clipboard"
|
||||||
data-clipboard-text="{{ alias_transfer_url }}">
|
data-clipboard-text="{{ alias_transfer_url }}">{{ alias_transfer_url }}</em>
|
||||||
{{ alias_transfer_url }}
|
|
||||||
</em>
|
|
||||||
<p class="mt-5">
|
<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.
|
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>
|
</p>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<br />
|
<br />
|
||||||
The period left in the current subscription isn't taken into account.
|
The period left in the current subscription isn't taken into account.
|
||||||
<br />
|
<br />
|
||||||
<a href="{{ url_for('dashboard.pricing') }}"
|
<a href="{{ url_for("dashboard.pricing") }}"
|
||||||
class="btn btn-primary mt-2">Re-subscribe</a>
|
class="btn btn-primary mt-2">Re-subscribe</a>
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -43,12 +43,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">PGP Public Key</label>
|
<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) -----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) -----BEGIN PGP PUBLIC KEY BLOCK-----">{{ contact.pgp_public_key or "" }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}
|
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}disabled{% endif %} value="save">Save</button>
|
||||||
disabled {% endif %} value="save">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{% if contact.pgp_finger_print %}
|
{% if contact.pgp_finger_print %}
|
||||||
|
|
||||||
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
|
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
|
||||||
|
@ -74,10 +74,7 @@
|
|||||||
required>
|
required>
|
||||||
{% for mailbox in mailboxes %}
|
{% for mailbox in mailboxes %}
|
||||||
|
|
||||||
<option value="{{ mailbox.id }}"
|
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
|
||||||
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
|
|
||||||
{{ mailbox.email }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="small-text">The mailbox(es) that owns this alias.</div>
|
<div class="small-text">The mailbox(es) that owns this alias.</div>
|
||||||
@ -102,7 +99,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$('.mailbox-select').multipleSelect();
|
$('.mailbox-select').multipleSelect();
|
||||||
|
|
||||||
|
@ -30,9 +30,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="alert alert-primary collapse {% if not custom_domains %} show{% endif %}"
|
<div class="alert alert-primary collapse {% if not custom_domains %}show{% endif %}" id="howtouse" role="alert">
|
||||||
id="howtouse"
|
|
||||||
role="alert">
|
|
||||||
By adding your domain, you can create aliases like <b>hi@my-domain.com</b>
|
By adding your domain, you can create aliases like <b>hi@my-domain.com</b>
|
||||||
<br />
|
<br />
|
||||||
You can also enable <b>catch-all</b> to create aliases on-the-fly:
|
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 %}
|
{% 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') }}"
|
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id, _anchor='dns-setup') }}"
|
||||||
class="btn btn-info btn-sm">
|
class="btn btn-info btn-sm">Ownership verified. Setup the DNS</a>
|
||||||
Ownership verified. Setup the DNS
|
|
||||||
</a>
|
|
||||||
{% elif custom_domain.ownership_verified and custom_domain.verified %}
|
{% elif custom_domain.ownership_verified and custom_domain.verified %}
|
||||||
<span class="badge badge-success">Domain ready</span>
|
<span class="badge badge-success">Domain ready</span>
|
||||||
<!-- custom_domain.ownership_verified is False -->
|
<!-- custom_domain.ownership_verified is False -->
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id, _anchor='ownership-form') }}"
|
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id, _anchor='ownership-form') }}"
|
||||||
class="btn btn-warning btn-sm"
|
class="btn btn-warning btn-sm"
|
||||||
role="button">
|
role="button">Verify domain ownership</a>
|
||||||
Verify domain ownership
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
<h6 class="card-subtitle mb-4 text-muted">
|
<h6 class="card-subtitle mb-4 text-muted">
|
||||||
@ -100,4 +94,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block script %}<script>$('.mailbox-select').multipleSelect();</script>{% endblock %}
|
|
||||||
|
@ -22,9 +22,7 @@
|
|||||||
|
|
||||||
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
|
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="alert alert-primary collapse {% if not dirs %} show{% endif %}"
|
<div class="alert alert-primary collapse {% if not dirs %}show{% endif %}" id="howtouse" role="alert">
|
||||||
id="howtouse"
|
|
||||||
role="alert">
|
|
||||||
<div>
|
<div>
|
||||||
Directory allows you to create aliases <b>on the fly</b>.
|
Directory allows you to create aliases <b>on the fly</b>.
|
||||||
</div>
|
</div>
|
||||||
@ -68,10 +66,10 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{{ toggle_dir_form.csrf_token }}
|
{{ toggle_dir_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="toggle-directory">
|
<input type="hidden" name="form-name" value="toggle-directory">
|
||||||
{{ toggle_dir_form.directory_id( type="hidden", value=dir.id) }}
|
{{ toggle_dir_form.directory_id(type="hidden", value=dir.id) }}
|
||||||
<label class="custom-switch cursor" style="padding-left: 1rem" data-toggle="tooltip" {% if dir.disabled %}
|
<label class="custom-switch cursor" style="padding-left: 1rem" data-toggle="tooltip" {% if dir.disabled %}
|
||||||
title="Enable directory on-the-fly alias creation" {% else %} title="Disable directory on-the-fly alias creation" {% endif %}>
|
title="Enable directory on-the-fly alias creation" {% else %} title="Disable directory on-the-fly alias creation" {% endif %}>
|
||||||
{{ toggle_dir_form.directory_enabled( class="custom-switch-input", checked=(not dir.disabled) ) }}
|
{{ toggle_dir_form.directory_enabled(class="custom-switch-input", checked=(not dir.disabled) ) }}
|
||||||
<span class="custom-switch-indicator"></span>
|
<span class="custom-switch-indicator"></span>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
@ -91,11 +89,11 @@
|
|||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="Aliases created with this directory are automatically owned by these mailboxes"></i>
|
title="Aliases created with this directory are automatically owned by these mailboxes"></i>
|
||||||
<br />
|
<br />
|
||||||
{% set dir_mailboxes=dir.mailboxes %}
|
{% set dir_mailboxes = dir.mailboxes %}
|
||||||
<form method="post" class="mt-2">
|
<form method="post" class="mt-2">
|
||||||
{{ update_dir_form.csrf_token }}
|
{{ update_dir_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="update">
|
<input type="hidden" name="form-name" value="update">
|
||||||
{{ update_dir_form.directory_id( type="hidden", value=dir.id) }}
|
{{ update_dir_form.directory_id(type="hidden", value=dir.id) }}
|
||||||
<select data-width="100%"
|
<select data-width="100%"
|
||||||
required
|
required
|
||||||
class="mailbox-select"
|
class="mailbox-select"
|
||||||
@ -103,10 +101,7 @@
|
|||||||
name="mailbox_ids">
|
name="mailbox_ids">
|
||||||
{% for mailbox in mailboxes %}
|
{% for mailbox in mailboxes %}
|
||||||
|
|
||||||
<option value="{{ mailbox.id }}"
|
<option value="{{ mailbox.id }}" {% if mailbox in dir_mailboxes %}selected{% endif %}>{{ mailbox.email }}</option>
|
||||||
{% if mailbox in dir_mailboxes %} selected{% endif %}>
|
|
||||||
{{ mailbox.email }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button class="mt-2 btn btn-outline-primary btn-sm">Update</button>
|
<button class="mt-2 btn btn-outline-primary btn-sm">Update</button>
|
||||||
@ -119,7 +114,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{{ delete_dir_form.csrf_token }}
|
{{ delete_dir_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="delete">
|
<input type="hidden" name="form-name" value="delete">
|
||||||
{{ delete_dir_form.directory_id( type="hidden", value=dir.id) }}
|
{{ delete_dir_form.directory_id(type="hidden", value=dir.id) }}
|
||||||
<span class="card-link btn btn-link float-right text-danger delete-dir">Delete</span>
|
<span class="card-link btn btn-link float-right text-danger delete-dir">Delete</span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row {% if current_user.directory_quota <= 0 %} disabled-content{% endif %}">
|
<div class="row {% if current_user.directory_quota <= 0 %}disabled-content{% endif %}">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -139,8 +134,8 @@
|
|||||||
<h2 class="h4 mb-1">New Directory</h2>
|
<h2 class="h4 mb-1">New Directory</h2>
|
||||||
<div class="small-text mb-4">You can create up to {{ current_user.directory_quota }} directories.</div>
|
<div class="small-text mb-4">You can create up to {{ current_user.directory_quota }} directories.</div>
|
||||||
{{ new_dir_form.name(class="form-control", placeholder="my-directory",
|
{{ new_dir_form.name(class="form-control", placeholder="my-directory",
|
||||||
pattern="[0-9a-z-_]{3,}",
|
pattern="[0-9a-z-_]{3,}",
|
||||||
title="Only letter, number, dash (-), underscore (_) can be used. Directory name must be at least 3 characters.") }}
|
title="Only letter, number, dash (-), underscore (_) can be used. Directory name must be at least 3 characters.") }}
|
||||||
{{ render_field_errors(new_dir_form.name) }}
|
{{ render_field_errors(new_dir_form.name) }}
|
||||||
<div class="small-text">
|
<div class="small-text">
|
||||||
Directory name must be at least 3 characters.
|
Directory name must be at least 3 characters.
|
||||||
@ -156,10 +151,7 @@
|
|||||||
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
|
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
|
||||||
{% for mailbox in mailboxes %}
|
{% for mailbox in mailboxes %}
|
||||||
|
|
||||||
<option value="{{ mailbox.id }}"
|
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
|
||||||
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
|
|
||||||
{{ mailbox.email }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button id="btn-create-directory" class="btn btn-primary mt-2">Create</button>
|
<button id="btn-create-directory" class="btn btn-primary mt-2">Create</button>
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
|
|
||||||
<div class="alert alert-warning mt-3">Rules are ineffective when catch-all is enabled.</div>
|
<div class="alert alert-warning mt-3">Rules are ineffective when catch-all is enabled.</div>
|
||||||
{% endif %}
|
{% 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">
|
<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.
|
For a greater control than a simple catch-all, you can define a set of <b>rules</b> to auto create aliases.
|
||||||
<br />
|
<br />
|
||||||
@ -60,8 +61,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Regex</label>
|
<label>Regex</label>
|
||||||
{{ new_auto_create_rule_form.regex(class="form-control",
|
{{ new_auto_create_rule_form.regex(class="form-control",
|
||||||
placeholder="prefix.*"
|
placeholder="prefix.*") }}
|
||||||
) }}
|
|
||||||
{{ render_field_errors(new_auto_create_rule_form.regex) }}
|
{{ render_field_errors(new_auto_create_rule_form.regex) }}
|
||||||
<div class="small-text">
|
<div class="small-text">
|
||||||
For example, if you want aliases that starts with <b>prefix</b> to be automatically created, you can set
|
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">
|
name="mailbox_ids">
|
||||||
{% for mailbox in mailboxes %}
|
{% for mailbox in mailboxes %}
|
||||||
|
|
||||||
<option value="{{ mailbox.id }}"
|
<option value="{{ mailbox.id }}" {% if mailbox.id == current_user.default_mailbox_id %}selected{% endif %}>{{ mailbox.email }}</option>
|
||||||
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
|
|
||||||
{{ mailbox.email }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -128,9 +125,7 @@
|
|||||||
{% if auto_create_test_result %}
|
{% if auto_create_test_result %}
|
||||||
|
|
||||||
<div class="alert {% if auto_create_test_passed %}
|
<div class="alert {% if auto_create_test_passed %}
|
||||||
alert-success {% else %} alert-warning {% endif %}">
|
alert-success {% else %} alert-warning {% endif %}">{{ auto_create_test_result }}</div>
|
||||||
{{ auto_create_test_result }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
Value: <em data-toggle="tooltip"
|
Value: <em data-toggle="tooltip"
|
||||||
title="Click to copy"
|
title="Click to copy"
|
||||||
class="clipboard"
|
class="clipboard"
|
||||||
data-clipboard-text="{{ custom_domain.get_ownership_dns_txt_value() }}">{{ custom_domain.get_ownership_dns_txt_value() }}</em>
|
data-clipboard-text="{{ ownership_record }}">{{ ownership_record }}</em>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="#ownership-form">
|
<form method="post" action="#ownership-form">
|
||||||
{{ csrf_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
@ -63,8 +63,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<hr />
|
<hr />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="{% if not custom_domain.ownership_verified %} disabled-content{% endif %}"
|
<div class="{% if not custom_domain.ownership_verified %}
|
||||||
id="dns-setup">
|
disabled-content{% endif %}" id="dns-setup">
|
||||||
{% if not custom_domain.ownership_verified %}
|
{% if not custom_domain.ownership_verified %}
|
||||||
|
|
||||||
<div class="alert alert-warning">A domain ownership must be verified first.</div>
|
<div class="alert alert-warning">A domain ownership must be verified first.</div>
|
||||||
@ -177,9 +177,7 @@
|
|||||||
<em data-toggle="tooltip"
|
<em data-toggle="tooltip"
|
||||||
title="Click to copy"
|
title="Click to copy"
|
||||||
class="clipboard"
|
class="clipboard"
|
||||||
data-clipboard-text="{{ spf_record }}">
|
data-clipboard-text="{{ spf_record }}">{{ spf_record }}</em>
|
||||||
{{ spf_record }}
|
|
||||||
</em>
|
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="#spf-form">
|
<form method="post" action="#spf-form">
|
||||||
{{ csrf_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
@ -238,10 +236,8 @@
|
|||||||
Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam
|
Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam
|
||||||
folder.
|
folder.
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">Add the following CNAME DNS records to your domain.</div>
|
||||||
Add the following CNAME DNS records to your domain.
|
{% for dkim_prefix, dkim_cname_value in dkim_records.items() %}
|
||||||
</div>
|
|
||||||
{% for dkim_prefix, dkim_cname_value in dkim_records %}
|
|
||||||
|
|
||||||
<div class="mb-2 p-3 dns-record">
|
<div class="mb-2 p-3 dns-record">
|
||||||
Record: CNAME
|
Record: CNAME
|
||||||
@ -256,9 +252,7 @@
|
|||||||
title="Click to copy"
|
title="Click to copy"
|
||||||
class="clipboard"
|
class="clipboard"
|
||||||
data-clipboard-text="{{ dkim_cname_value }}."
|
data-clipboard-text="{{ dkim_cname_value }}."
|
||||||
style="overflow-wrap: break-word">
|
style="overflow-wrap: break-word">{{ dkim_cname_value }}.</em>
|
||||||
{{ dkim_cname_value }}.
|
|
||||||
</em>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
@ -282,21 +276,15 @@
|
|||||||
<input type="hidden" name="form-name" value="check-dkim">
|
<input type="hidden" name="form-name" value="check-dkim">
|
||||||
{% if custom_domain.dkim_verified %}
|
{% if custom_domain.dkim_verified %}
|
||||||
|
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<button type="submit" class="btn btn-outline-primary">Re-verify</button>
|
||||||
Re-verify
|
|
||||||
</button>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">Verify</button>
|
||||||
Verify
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if not dkim_ok %}
|
{% if not dkim_ok %}
|
||||||
|
|
||||||
<div class="text-danger mt-4">
|
<div class="text-danger mt-4">
|
||||||
<p>
|
<p>Your DNS is not correctly set.</p>
|
||||||
Your DNS is not correctly set.
|
|
||||||
</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for custom_record, retrieved_cname in dkim_errors.items() %}
|
{% for custom_record, retrieved_cname in dkim_errors.items() %}
|
||||||
|
|
||||||
@ -312,10 +300,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if custom_domain.dkim_verified %}
|
{% if custom_domain.dkim_verified %}
|
||||||
|
|
||||||
<div class="text-danger mt-4">
|
<div class="text-danger mt-4">DKIM is still enabled. Please update your DKIM settings with all CNAME records</div>
|
||||||
DKIM is still enabled. Please update your DKIM settings with all CNAME records
|
{% endif %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
@ -330,24 +316,20 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="cursor"
|
<span class="cursor"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
data-original-title="DMARC Not Verified">🚫 </span>
|
data-original-title="DMARC Not Verified">🚫</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
DMARC
|
DMARC
|
||||||
<a href="https://en.wikipedia.org/wiki/DMARC"
|
<a href="https://en.wikipedia.org/wiki/DMARC"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">(Wikipedia↗)</a>
|
||||||
(Wikipedia↗)
|
|
||||||
</a>
|
|
||||||
is designed to protect the domain from unauthorized use, commonly known as email spoofing.
|
is designed to protect the domain from unauthorized use, commonly known as email spoofing.
|
||||||
<br />
|
<br />
|
||||||
Built around SPF and DKIM, a DMARC policy tells the receiving mail server what to do if
|
Built around SPF and DKIM, a DMARC policy tells the receiving mail server what to do if
|
||||||
neither of those authentication methods passes.
|
neither of those authentication methods passes.
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">Add the following TXT DNS record to your domain.</div>
|
||||||
Add the following TXT DNS record to your domain.
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 p-3 dns-record">
|
<div class="mb-2 p-3 dns-record">
|
||||||
Record: TXT
|
Record: TXT
|
||||||
<br />
|
<br />
|
||||||
@ -360,9 +342,7 @@
|
|||||||
<em data-toggle="tooltip"
|
<em data-toggle="tooltip"
|
||||||
title="Click to copy"
|
title="Click to copy"
|
||||||
class="clipboard"
|
class="clipboard"
|
||||||
data-clipboard-text="{{ dmarc_record }}">
|
data-clipboard-text="{{ dmarc_record }}">{{ dmarc_record }}</em>
|
||||||
{{ dmarc_record }}
|
|
||||||
</em>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
Some DNS registrar might require a full record path, in this case please use
|
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">
|
<input type="hidden" name="form-name" value="check-dmarc">
|
||||||
{% if custom_domain.dmarc_verified %}
|
{% if custom_domain.dmarc_verified %}
|
||||||
|
|
||||||
<button type="submit" class="btn btn-outline-primary">
|
<button type="submit" class="btn btn-outline-primary">Re-verify</button>
|
||||||
Re-verify
|
|
||||||
</button>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">Verify</button>
|
||||||
Verify
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if not dmarc_ok %}
|
{% if not dmarc_ok %}
|
||||||
|
@ -34,13 +34,14 @@
|
|||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
Auto-created aliases are automatically owned by the following mailboxes
|
Auto-created aliases are automatically owned by the following mailboxes
|
||||||
<i class="fe fe-corner-right-down"></i>
|
<i class="fe fe-corner-right-down"></i>
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
{% set domain_mailboxes=custom_domain.mailboxes %}
|
{% set domain_mailboxes = custom_domain.mailboxes %}
|
||||||
<form method="post" class="mt-2">
|
<form method="post" class="mt-2">
|
||||||
{{ csrf_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
<input type="hidden" name="form-name" value="update">
|
<input type="hidden" name="form-name" value="update">
|
||||||
@ -54,10 +55,7 @@
|
|||||||
name="mailbox_ids">
|
name="mailbox_ids">
|
||||||
{% for mailbox in mailboxes %}
|
{% for mailbox in mailboxes %}
|
||||||
|
|
||||||
<option value="{{ mailbox.id }}"
|
<option value="{{ mailbox.id }}" {% if mailbox in domain_mailboxes %}selected{% endif %}>{{ mailbox.email }}</option>
|
||||||
{% if mailbox in domain_mailboxes %} selected{% endif %}>
|
|
||||||
{{ mailbox.email }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<p>Alternatively you can use your Proton credentials to ensure it's you.</p>
|
<p>Alternatively you can use your Proton credentials to ensure it's you.</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary btn-block mt-2 proton-button w-25"
|
<a class="btn btn-primary btn-block mt-2 proton-button"
|
||||||
href="{{ url_for('auth.proton_login', next=next) }}">
|
href="{{ url_for('auth.proton_login', next=next) }}" style="max-width: 400px">
|
||||||
<img class="mr-2" src="/static/images/proton.svg" />
|
<img class="mr-2" src="/static/images/proton.svg" />
|
||||||
Authenticate with Proton
|
Authenticate with Proton
|
||||||
</a>
|
</a>
|
||||||
@ -38,4 +38,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -45,7 +45,7 @@
|
|||||||
<td>Link a New Key</td>
|
<td>Link a New Key</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td class="text-center">
|
<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>
|
<button class="btn btn-outline-success">Link</button>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -61,8 +61,7 @@
|
|||||||
class="btn btn-success dropdown-toggle btn-group-border-left"
|
class="btn btn-success dropdown-toggle btn-group-border-left"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false">
|
aria-expanded="false"></button>
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right border-left"
|
<div class="dropdown-menu dropdown-menu-right border-left"
|
||||||
aria-labelledby="btnGroupDrop1">
|
aria-labelledby="btnGroupDrop1">
|
||||||
<div>
|
<div>
|
||||||
@ -125,7 +124,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Aliases</div>
|
<div class="subheader">Aliases</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ stats.nb_alias }}</div>
|
<div class="h1 m-0">{{ stats.nb_alias }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -137,7 +138,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Forwarded</div>
|
<div class="subheader">Forwarded</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ stats.nb_forward }}</div>
|
<div class="h1 m-0">{{ stats.nb_forward }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -149,7 +152,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Replies/Sent</div>
|
<div class="subheader">Replies/Sent</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ stats.nb_reply }}</div>
|
<div class="h1 m-0">{{ stats.nb_reply }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -161,7 +166,9 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="subheader">Blocked</div>
|
<div class="subheader">Blocked</div>
|
||||||
<div class="text-muted"
|
<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>
|
||||||
<div class="h1 m-0">{{ stats.nb_block }}</div>
|
<div class="h1 m-0">{{ stats.nb_block }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,52 +184,28 @@
|
|||||||
<select name="sort"
|
<select name="sort"
|
||||||
onchange="this.form.submit()"
|
onchange="this.form.submit()"
|
||||||
class="form-control mr-3 shadow">
|
class="form-control mr-3 shadow">
|
||||||
<option value="" {% if sort == "" %} selected{% endif %}>
|
<option value="" {% if sort == "" %}selected{% endif %}>Sort by most recent activity</option>
|
||||||
Sort by most recent activity
|
<option value="old2new" {% if sort == "old2new" %}selected{% endif %}>Alias Old-Recent</option>
|
||||||
</option>
|
<option value="new2old" {% if sort == "new2old" %}selected{% endif %}>Alias Recent-Old</option>
|
||||||
<option value="old2new" {% if sort == "old2new" %} selected{% endif %}>
|
<option value="a2z" {% if sort == "a2z" %}selected{% endif %}>Alias A-Z</option>
|
||||||
Alias Old-Recent
|
<option value="z2a" {% if sort == "z2a" %}selected{% endif %}>Alias Z-A</option>
|
||||||
</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>
|
||||||
<select name="filter"
|
<select name="filter"
|
||||||
onchange="this.form.submit()"
|
onchange="this.form.submit()"
|
||||||
class="form-control mr-3 shadow"
|
class="form-control mr-3 shadow"
|
||||||
style="max-width: 200px">
|
style="max-width: 200px">
|
||||||
<option value="" {% if filter == "" %} selected{% endif %}>
|
<option value="" {% if filter == "" %}selected{% endif %}>All Aliases</option>
|
||||||
All Aliases
|
<option value="pinned" {% if filter == "pinned" %}selected{% endif %}>Pinned Aliases</option>
|
||||||
</option>
|
<option value="enabled" {% if filter == "enabled" %}selected{% endif %}>Only Enabled Aliases</option>
|
||||||
<option value="pinned" {% if filter == "pinned" %} selected{% endif %}>
|
<option value="disabled" {% if filter == "disabled" %}selected{% endif %}>Only Disabled Aliases</option>
|
||||||
Pinned Aliases
|
<option value="hibp" {% if filter == "hibp" %}selected{% endif %}>Only Aliases Found In Data Breaches</option>
|
||||||
</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() %}
|
{% for mailbox in current_user.mailboxes() %}
|
||||||
|
|
||||||
<option value="mailbox:{{ mailbox.id }}" {% if filter == "mailbox:" ~ mailbox.id %}
|
<option value="mailbox:{{ mailbox.id }}" {% if filter == "mailbox:" ~ mailbox.id %}selected{% endif %}>{{ mailbox.email }}'s aliases</option>
|
||||||
selected {% endif %}>
|
|
||||||
{{ mailbox.email }}'s aliases
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for directory in current_user.directories %}
|
{% for directory in current_user.directories %}
|
||||||
|
|
||||||
<option value="directory:{{ directory.id }}" {% if filter == "directory:" ~ directory.id %}
|
<option value="directory:{{ directory.id }}" {% if filter == "directory:" ~ directory.id %}selected{% endif %}>
|
||||||
selected {% endif %}>
|
|
||||||
Directory <b>{{ directory.name }}</b> aliases
|
Directory <b>{{ directory.name }}</b> aliases
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -237,7 +220,7 @@
|
|||||||
<div style="margin-left: auto">
|
<div style="margin-left: auto">
|
||||||
{% if query or sort or filter %}
|
{% 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>
|
class="btn btn-outline-secondary">Reset</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -251,10 +234,11 @@
|
|||||||
|
|
||||||
{% set alias = alias_info.alias %}
|
{% set alias = alias_info.alias %}
|
||||||
<div class="col-12 col-lg-6" id="alias-container-{{ alias.id }}">
|
<div class="col-12 col-lg-6" id="alias-container-{{ alias.id }}">
|
||||||
<div class="card p-4 shadow-sm {% if alias.id == highlight_alias_id %} highlight-row{% endif %} ">
|
<div class="card p-4 shadow-sm {% if alias.id == highlight_alias_id %}highlight-row{% endif %} ">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-8">
|
<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>.
|
data-intro="This is your first <em>alias</em>.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@ -358,7 +342,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
<div class="flex-grow-1 mr-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>
|
||||||
</div>
|
</div>
|
||||||
<!-- Send Email && More button -->
|
<!-- Send Email && More button -->
|
||||||
@ -399,27 +391,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- END Send Email && More button -->
|
<!-- END Send Email && More button -->
|
||||||
<!-- Collapse section -->
|
<!-- Collapse section -->
|
||||||
<div class="{% if not current_user.expand_alias_info %} collapse{% endif %} mt-2"
|
<div class="{% if not current_user.expand_alias_info %}
|
||||||
id="alias-{{ alias.id }}">
|
collapse{% endif %} mt-2" id="alias-{{ alias.id }}">
|
||||||
{% if alias_info.latest_email_log != None %}
|
{% if alias_info.latest_email_log != None %}
|
||||||
|
|
||||||
<div style="font-size: 12px">
|
<div style="font-size: 12px">Alias created {{ alias.created_at | dt }}</div>
|
||||||
Alias created {{ alias.created_at | dt }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwarded,
|
<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_blocked }}</span> blocked,
|
||||||
<span class="alias-activity">{{ alias_info.nb_reply }}</span> sent
|
<span class="alias-activity">{{ alias_info.nb_reply }}</span> sent
|
||||||
in the last 14 days
|
in the last 14 days
|
||||||
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
|
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
|
||||||
class="btn btn-sm btn-link">
|
class="btn btn-sm btn-link">See All →</a>
|
||||||
See All →
|
|
||||||
</a>
|
|
||||||
{% if mailboxes|length > 1 %}
|
{% if mailboxes|length > 1 %}
|
||||||
|
|
||||||
<div class="small-text">
|
<div class="small-text">Current mailbox</div>
|
||||||
Current mailbox
|
|
||||||
</div>
|
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="flex-grow-1 mr-2">
|
<div class="flex-grow-1 mr-2">
|
||||||
<select required
|
<select required
|
||||||
@ -431,10 +417,7 @@
|
|||||||
onchange="handleMailboxChange({{ alias.id }}, '{{ alias.email }}')">
|
onchange="handleMailboxChange({{ alias.id }}, '{{ alias.email }}')">
|
||||||
{% for mailbox in mailboxes %}
|
{% for mailbox in mailboxes %}
|
||||||
|
|
||||||
<option value="{{ mailbox.id }}" {% if alias_info.contain_mailbox(mailbox.id) %}
|
<option value="{{ mailbox.id }}" {% if alias_info.contain_mailbox(mailbox.id) %}selected{% endif %}>{{ mailbox.email }}</option>
|
||||||
selected {% endif %}>
|
|
||||||
{{ mailbox.email }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -502,11 +485,7 @@
|
|||||||
<input type="hidden" name="form-name" value="delete-alias">
|
<input type="hidden" name="form-name" value="delete-alias">
|
||||||
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
<input type="hidden" name="alias-id" value="{{ alias.id }}">
|
||||||
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
|
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
|
||||||
<span class="btn btn-link btn-sm float-right text-danger"
|
<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 }}">
|
||||||
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 <i class="dropdown-icon fe fe-trash-2 text-danger"></i>
|
Delete <i class="dropdown-icon fe fe-trash-2 text-danger"></i>
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
@ -527,14 +506,12 @@
|
|||||||
<nav aria-label="Alias navigation">
|
<nav aria-label="Alias navigation">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
<li class="page-item mr-1">
|
<li class="page-item mr-1">
|
||||||
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}"
|
<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) }}">
|
||||||
href="{{ url_for('dashboard.index', page=page-1, query=query, sort=sort, filter=filter) }}">
|
|
||||||
Previous
|
Previous
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}"
|
<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) }}">
|
||||||
href="{{ url_for('dashboard.index', page=page+1, query=query, sort=sort, filter=filter) }}">
|
|
||||||
Next
|
Next
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -22,21 +22,19 @@
|
|||||||
|
|
||||||
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
|
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %} show{% endif %}"
|
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %}show{% endif %}" id="howtouse" role="alert">
|
||||||
id="howtouse"
|
|
||||||
role="alert">
|
|
||||||
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose
|
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose
|
||||||
the
|
the
|
||||||
mailbox that <em>owns</em> this alias, i.e:
|
mailbox that <em>owns</em> this alias, i.e:
|
||||||
<br/>
|
<br />
|
||||||
- all emails sent to this alias will be forwarded to this mailbox
|
- all emails sent to this alias will be forwarded to this mailbox
|
||||||
<br/>
|
<br />
|
||||||
- from this mailbox, you can reply/send emails from the alias.
|
- from this mailbox, you can reply/send emails from the alias.
|
||||||
<br/>
|
<br />
|
||||||
<br/>
|
<br />
|
||||||
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
|
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
|
||||||
<br/>
|
<br />
|
||||||
<br/>
|
<br />
|
||||||
The mailbox doesn't have to be your email: it can be your friend's email
|
The mailbox doesn't have to be your email: it can be your friend's email
|
||||||
if you want to create aliases for your buddy.
|
if you want to create aliases for your buddy.
|
||||||
</div>
|
</div>
|
||||||
@ -75,9 +73,9 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<h6 class="card-subtitle mb-2 text-muted">
|
<h6 class="card-subtitle mb-2 text-muted">
|
||||||
Created {{ mailbox.created_at | dt }}
|
Created {{ mailbox.created_at | dt }}
|
||||||
<br/>
|
<br />
|
||||||
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases.
|
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases.
|
||||||
<br/>
|
<br />
|
||||||
</h6>
|
</h6>
|
||||||
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit
|
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit
|
||||||
➡</a>
|
➡</a>
|
||||||
@ -92,9 +90,7 @@
|
|||||||
<input type="hidden" name="form-name" value="set-default">
|
<input type="hidden" name="form-name" value="set-default">
|
||||||
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
||||||
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
|
<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 %}">
|
<button class="card-link btn btn-link {% if mailbox.id == current_user.default_mailbox_id %}disabled{% endif %}">Set As Default Mailbox</button>
|
||||||
Set As Default Mailbox
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -105,22 +101,16 @@
|
|||||||
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
|
||||||
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
|
<input type="hidden" name="mailbox_id" value="{{ mailbox.id }}">
|
||||||
<select hidden name="transfer_mailbox_id" value="">
|
<select hidden name="transfer_mailbox_id" value="">
|
||||||
<option value="-1">
|
<option value="-1">Delete my aliases</option>
|
||||||
Delete my aliases
|
|
||||||
</option>
|
|
||||||
{% for mailbox_opt in mailboxes %}
|
{% for mailbox_opt in mailboxes %}
|
||||||
|
|
||||||
{% if mailbox_opt.verified and mailbox_opt.id != mailbox.id %}
|
{% if mailbox_opt.verified and mailbox_opt.id != mailbox.id %}
|
||||||
|
|
||||||
<option value="{{ mailbox_opt.id }}">
|
<option value="{{ mailbox_opt.id }}">{{ mailbox_opt.email }}</option>
|
||||||
{{ mailbox_opt.email }}
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<span class="card-link btn btn-link text-danger float-right delete-mailbox {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
|
<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>
|
||||||
Delete
|
|
||||||
</span>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,9 +60,7 @@
|
|||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<span class="text-danger">Pending change: {{ pending_email }}</span>
|
<span class="text-danger">Pending change: {{ pending_email }}</span>
|
||||||
<a href="{{ url_for('dashboard.cancel_mailbox_change_route', mailbox_id=mailbox.id) }}"
|
<a href="{{ url_for('dashboard.cancel_mailbox_change_route', mailbox_id=mailbox.id) }}"
|
||||||
class="btn btn-secondary btn-sm">
|
class="btn btn-secondary btn-sm">Cancel mailbox change</a>
|
||||||
Cancel mailbox change
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -123,13 +121,15 @@
|
|||||||
{{ csrf_form.csrf_token }}
|
{{ csrf_form.csrf_token }}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">PGP Public Key</label>
|
<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) -----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) -----BEGIN PGP PUBLIC KEY BLOCK-----">{{ mailbox.pgp_public_key or "" }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="form-name" value="pgp">
|
<input type="hidden" name="form-name" value="pgp">
|
||||||
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}
|
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}disabled{% endif %} value="save">Save</button>
|
||||||
disabled {% endif %} value="save">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{% if mailbox.pgp_finger_print %}
|
{% if mailbox.pgp_finger_print %}
|
||||||
|
|
||||||
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
|
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
Mailbox <b>{{ mailbox.email }}</b> verified, you can now start creating alias with it
|
Mailbox <b>{{ mailbox.email }}</b> verified, you can now start creating alias with it
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -17,13 +17,9 @@
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
text-overflow: ellipsis;">
|
text-overflow: ellipsis">{{ notification.message | safe }}</div>
|
||||||
{{ notification.message | safe }}
|
|
||||||
</div>
|
|
||||||
<a href="{{ url_for('dashboard.notification_route', notification_id=notification.id) }}"
|
<a href="{{ url_for('dashboard.notification_route', notification_id=notification.id) }}"
|
||||||
class="mt-2 btn btn-outline-primary">
|
class="mt-2 btn btn-outline-primary">More ➡</a>
|
||||||
More ➡
|
|
||||||
</a>
|
|
||||||
<div class="small text-muted mt-2">{{ notification.created_at | dt }}</div>
|
<div class="small text-muted mt-2">{{ notification.created_at | dt }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -36,16 +32,12 @@
|
|||||||
<nav aria-label="Notification navigation">
|
<nav aria-label="Notification navigation">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
<li class="page-item mr-1">
|
<li class="page-item mr-1">
|
||||||
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}"
|
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}" href="{{ url_for('dashboard.notifications_route', page=page-1) }}">
|
||||||
href="{{ url_for('dashboard.notifications_route', page=page-1) }}">
|
|
||||||
Previous
|
Previous
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}"
|
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}" href="{{ url_for('dashboard.notifications_route', page=page+1) }}">Next</a>
|
||||||
href="{{ url_for('dashboard.notifications_route', page=page+1) }}">
|
|
||||||
Next
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
if (window.Paddle === undefined) {
|
if (window.Paddle === undefined) {
|
||||||
console.log("cannot load Paddle from CDN");
|
console.log("cannot load Paddle from CDN");
|
||||||
// split string to avoid djlint incorrectly formatting the file
|
// split string to avoid djlint incorrectly formatting the file
|
||||||
document.write('<' + 'script src="/static/vendor/paddle.js"><\/script' + '>');
|
document.write('<' + 'script src="/static/vendor/paddle.js"><\/script ' + '>');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@ -144,9 +144,7 @@
|
|||||||
{% set sub = current_user.get_paddle_subscription() %}
|
{% 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"
|
<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"
|
aria-disabled="true"
|
||||||
disabled>
|
disabled>Current plan</button>
|
||||||
Current plan
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
@ -172,9 +170,7 @@
|
|||||||
<div class="h3 my-3">$4 / month</div>
|
<div class="h3 my-3">$4 / month</div>
|
||||||
<div class="text-center mt-4 mb-6">
|
<div class="text-center mt-4 mb-6">
|
||||||
<button class="btn btn-primary btn-lg w-100"
|
<button class="btn btn-primary btn-lg w-100"
|
||||||
onclick="upgradePaddle({{ PADDLE_MONTHLY_PRODUCT_ID }})">
|
onclick="upgradePaddle({{ PADDLE_MONTHLY_PRODUCT_ID }})">Upgrade to Premium</button>
|
||||||
Upgrade to Premium
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
@ -287,9 +283,7 @@
|
|||||||
{% set sub = current_user.get_paddle_subscription() %}
|
{% 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"
|
<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"
|
aria-disabled="true"
|
||||||
disabled>
|
disabled>Current plan</button>
|
||||||
Current plan
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
@ -315,9 +309,7 @@
|
|||||||
<div class="h3 my-3">$30 / year</div>
|
<div class="h3 my-3">$30 / year</div>
|
||||||
<div class="text-center mt-4 mb-6">
|
<div class="text-center mt-4 mb-6">
|
||||||
<button class="btn btn-primary btn-lg w-100"
|
<button class="btn btn-primary btn-lg w-100"
|
||||||
onclick="upgradePaddle({{ PADDLE_YEARLY_PRODUCT_ID }})">
|
onclick="upgradePaddle({{ PADDLE_YEARLY_PRODUCT_ID }})">Upgrade to Premium</button>
|
||||||
Upgrade to Premium
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<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:
|
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>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>Cards (including Mastercard, Visa, Maestro, American Express, Discover, Diners Club, JCB, UnionPay, and Mada)</li>
|
||||||
Cards (including Mastercard, Visa, Maestro, American Express, Discover, Diners Club, JCB, UnionPay, and Mada)
|
<li>PayPal</li>
|
||||||
</li>
|
<li>Apple Pay</li>
|
||||||
<li>
|
<li>Wire Transfers (ACH/SEPA/BACS)</li>
|
||||||
PayPal
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Apple Pay
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Wire Transfers (ACH/SEPA/BACS)
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
More information can be found on
|
More information can be found on
|
||||||
@ -482,7 +466,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<a class="btn btn-outline-primary text-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"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
Upgrade to Premium - cryptocurrency
|
Upgrade to Premium - cryptocurrency
|
||||||
@ -520,7 +504,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
To redeem or buy a coupon, please go to the
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -554,18 +538,12 @@
|
|||||||
sending emails. Concretely:
|
sending emails. Concretely:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>All aliases/domains/directories/mailboxes you have created are kept and continue working normally.</li>
|
||||||
All aliases/domains/directories/mailboxes you have created are kept and continue working normally.
|
<li>You cannot create new aliases if you exceed the free plan limit, i.e. have more than 10 aliases.</li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
You cannot create new aliases if you exceed the free plan limit, i.e. have more than 10 aliases.
|
|
||||||
</li>
|
|
||||||
<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.
|
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>
|
||||||
<li>
|
<li>You cannot add new domain, directory or mailbox.</li>
|
||||||
You cannot add new domain, directory or mailbox.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<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.
|
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"
|
aria-labelledby="pricing-faq-question-discounts"
|
||||||
data-parent="#pricing-faq">
|
data-parent="#pricing-faq">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>We offer important discounts or free premium for:</p>
|
||||||
We offer important discounts or free premium for:
|
|
||||||
</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>students, professors or technical staffs working at an educational institute</li>
|
||||||
students, professors or technical staffs working at an educational institute
|
<li>activists, dissidents or journalists</li>
|
||||||
</li>
|
<li>charity organizations</li>
|
||||||
<li>
|
|
||||||
activists, dissidents or journalists
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
charity organizations
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
Please send us an email at <a href="mailto:support@simplelogin.zendesk.com" target="_blank">support@simplelogin.zendesk.com</a> for more info.
|
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"
|
aria-labelledby="pricing-faq-question-refund"
|
||||||
data-parent="#pricing-faq">
|
data-parent="#pricing-faq">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>No we don't have a refund policy because SimpleLogin has a trial period where you can try all premium features.</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% for referral in referrals %}
|
{% for referral in referrals %}
|
||||||
|
|
||||||
<div class="card p-4 shadow-sm {% if referral.id == highlight_id %} highlight-row{% endif %}">
|
<div class="card p-4 shadow-sm {% if referral.id == highlight_id %}highlight-row{% endif %}">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="form-name" value="update">
|
<input type="hidden" name="form-name" value="update">
|
||||||
<input type="hidden" name="referral-id" value="{{ referral.id }}">
|
<input type="hidden" name="referral-id" value="{{ referral.id }}">
|
||||||
@ -102,9 +102,7 @@
|
|||||||
title="Click to copy"
|
title="Click to copy"
|
||||||
class="clipboard"
|
class="clipboard"
|
||||||
data-clipboard-text="{{ '?slref=' + referral.code }}"
|
data-clipboard-text="{{ '?slref=' + referral.code }}"
|
||||||
style="overflow-wrap: break-word">
|
style="overflow-wrap: break-word">?slref={{ referral.code }}</em>
|
||||||
?slref={{ referral.code }}
|
|
||||||
</em>
|
|
||||||
to any link on SimpleLogin website.
|
to any link on SimpleLogin website.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
{% set refused_email = email_log.refused_email %}
|
{% set refused_email = email_log.refused_email %}
|
||||||
{% set contact = email_log.contact %}
|
{% set contact = email_log.contact %}
|
||||||
{% set alias = contact.alias %}
|
{% set alias = contact.alias %}
|
||||||
<div class="card p-4 shadow-sm {% if email_log.id == highlight_id %} highlight-row{% endif %}">
|
<div class="card p-4 shadow-sm {% if email_log.id == highlight_id %}highlight-row{% endif %}">
|
||||||
<div class="small-text">
|
<div class="small-text">
|
||||||
Sent {{ refused_email.created_at | dt }}
|
Sent {{ refused_email.created_at | dt }}
|
||||||
{% if email_log.bounced %}
|
{% if email_log.bounced %}
|
||||||
@ -70,9 +70,7 @@
|
|||||||
To: {{ alias.email }}
|
To: {{ alias.email }}
|
||||||
<a href='{{ url_for("dashboard.index", highlight_alias_id=alias.id) }}'
|
<a href='{{ url_for("dashboard.index", highlight_alias_id=alias.id) }}'
|
||||||
class="text-danger small-text"
|
class="text-danger small-text"
|
||||||
style="text-decoration: underline">
|
style="text-decoration: underline">Disable Alias</a>
|
||||||
Disable Alias
|
|
||||||
</a>
|
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if refused_email.deleted %}
|
{% if refused_email.deleted %}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -23,16 +23,14 @@
|
|||||||
|
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="alert alert-danger" role="alert">
|
||||||
This feature is only available on Premium plan.
|
This feature is only available on Premium plan.
|
||||||
<a href="{{ url_for('dashboard.pricing') }}"
|
<a href="{{ url_for("dashboard.pricing") }}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
Upgrade<i class="fe fe-external-link"></i>
|
Upgrade<i class="fe fe-external-link"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="alert alert-primary collapse {% if not subdomains %} show{% endif %}"
|
<div class="alert alert-primary collapse {% if not subdomains %}show{% endif %}" id="howtouse" role="alert">
|
||||||
id="howtouse"
|
|
||||||
role="alert">
|
|
||||||
You can use subdomain to quickly create email aliases without opening SimpleLogin app.
|
You can use subdomain to quickly create email aliases without opening SimpleLogin app.
|
||||||
<br />
|
<br />
|
||||||
Handy when you need to quickly give out an email address, for example on a phone call, in a meeting or just
|
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>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row {% if current_user.subdomain_quota <= 0 %} disabled-content{% endif %}"
|
<div class="row {% if current_user.subdomain_quota <= 0 %}disabled-content{% endif %}" id="new-subdomain">
|
||||||
id="new-subdomain">
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -88,12 +85,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Root domain</label>
|
<label>Root domain</label>
|
||||||
<select name="domain" v-model="domain" class="form-control">
|
<select name="domain" v-model="domain" class="form-control">
|
||||||
{% for sl_domain in sl_domains %}
|
{% for sl_domain in sl_domains %}<option value="{{ sl_domain.domain }}">{{ sl_domain.domain }}</option>{% endfor %}
|
||||||
|
|
||||||
<option value="{{ sl_domain.domain }}">
|
|
||||||
{{ sl_domain.domain }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary">Create</button>
|
<button class="btn btn-primary">Create</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "default.html" %}
|
{% extends "default.html" %}
|
||||||
|
|
||||||
{% set active_page = 'dashboard' %}
|
{% set active_page = "dashboard" %}
|
||||||
{% block title %}Support{% endblock %}
|
{% block title %}Support{% endblock %}
|
||||||
{% block default_content %}
|
{% block default_content %}
|
||||||
|
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<div class="card-title mb-3">Report a problem</div>
|
<div class="card-title mb-3">Report a problem</div>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
If an email cannot be delivered to your mailbox, please check
|
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
|
your
|
||||||
notifications
|
notifications
|
||||||
</a>
|
</a>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<br />
|
<br />
|
||||||
For generic questions, i.e. not related to your account, we recommend to post the question on
|
For generic questions, i.e. not related to your account, we recommend to post the question on
|
||||||
our
|
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
|
where our community can help answer the question
|
||||||
and other people with the same question can find the answer there.
|
and other people with the same question can find the answer there.
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +28,12 @@
|
|||||||
<form id="supportZendeskForm" method="post" enctype="multipart/form-data">
|
<form id="supportZendeskForm" method="post" enctype="multipart/form-data">
|
||||||
<div class="mt-4 mb-5">
|
<div class="mt-4 mb-5">
|
||||||
<label for="issueDescription" class="form-label font-weight-bold">What happened?</label>
|
<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>
|
||||||
<div class="mt-5 font-weight-bold">Attach files to support request</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>
|
<div class="text-muted">Only images, text and emails are accepted</div>
|
||||||
@ -57,9 +62,7 @@
|
|||||||
<button class="btn btn-outline-primary"
|
<button class="btn btn-outline-primary"
|
||||||
type="button"
|
type="button"
|
||||||
@click="generateRandomAlias"
|
@click="generateRandomAlias"
|
||||||
id="button-addon2">
|
id="button-addon2">Generate a random alias</button>
|
||||||
Generate a random alias
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
|
@ -64,9 +64,9 @@
|
|||||||
{# <div class="form-group">#}
|
{# <div class="form-group">#}
|
||||||
{# <label class="form-label">Tell us about your app</label>#}
|
{# <label class="form-label">Tell us about your app</label>#}
|
||||||
{# {{ approval_form.description(#}
|
{# {{ approval_form.description(#}
|
||||||
{# class="form-control", rows="10",#}
|
{# class="form-control", rows="10",#}
|
||||||
{# placeholder="This information is used for approving your application. Please give us as much info as you can, for example where you plan to use SimpleLogin, for which community, etc."#}
|
{# placeholder="This information is used for approving your application. Please give us as much info as you can, for example where you plan to use SimpleLogin, for which community, etc."#}
|
||||||
{# ) }}#}
|
{#) }}#}
|
||||||
{# {{ render_field_errors(approval_form.description) }}#}
|
{# {{ render_field_errors(approval_form.description) }}#}
|
||||||
{# </div>#}
|
{# </div>#}
|
||||||
{##}
|
{##}
|
||||||
|
@ -7,14 +7,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">OpenID Connect Discovery Document</label>
|
<label class="form-label">OpenID Connect Discovery Document</label>
|
||||||
<div class="input-group mt-2">
|
<div class="input-group mt-2">
|
||||||
<input type="text"
|
<input type="text" disabled value="{{ URL + "/.well-known/openid-configuration" }}" class="form-control">
|
||||||
disabled
|
|
||||||
value="{{ URL + "/.well-known/openid-configuration" }}"
|
|
||||||
class="form-control">
|
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}"
|
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}" class="clipboard btn btn-primary" type="button">
|
||||||
class="clipboard btn btn-primary"
|
|
||||||
type="button">
|
|
||||||
<i class="fe fe-clipboard"></i>
|
<i class="fe fe-clipboard"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@ -23,14 +18,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Authorization endpoint</label>
|
<label class="form-label">Authorization endpoint</label>
|
||||||
<div class="input-group mt-2">
|
<div class="input-group mt-2">
|
||||||
<input type="text"
|
<input type="text" disabled value="{{ URL + "/oauth2/authorize" }}" class="form-control">
|
||||||
disabled
|
|
||||||
value="{{ URL + "/oauth2/authorize" }}"
|
|
||||||
class="form-control">
|
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button data-clipboard-text="{{ URL + "/oauth2/authorize" }}"
|
<button data-clipboard-text="{{ URL + "/oauth2/authorize" }}" class="clipboard btn btn-primary" type="button">
|
||||||
class="clipboard btn btn-primary"
|
|
||||||
type="button">
|
|
||||||
<i class="fe fe-clipboard"></i>
|
<i class="fe fe-clipboard"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@ -39,14 +29,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Token endpoint</label>
|
<label class="form-label">Token endpoint</label>
|
||||||
<div class="input-group mt-2">
|
<div class="input-group mt-2">
|
||||||
<input type="text"
|
<input type="text" disabled value="{{ URL + "/oauth2/token" }}" class="form-control">
|
||||||
disabled
|
|
||||||
value="{{ URL + "/oauth2/token" }}"
|
|
||||||
class="form-control">
|
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button data-clipboard-text="{{ URL + "/oauth2/token" }}"
|
<button data-clipboard-text="{{ URL + "/oauth2/token" }}" class="clipboard btn btn-primary" type="button">
|
||||||
class="clipboard btn btn-primary"
|
|
||||||
type="button">
|
|
||||||
<i class="fe fe-clipboard"></i>
|
<i class="fe fe-clipboard"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@ -55,14 +40,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">UserInfo endpoint</label>
|
<label class="form-label">UserInfo endpoint</label>
|
||||||
<div class="input-group mt-2">
|
<div class="input-group mt-2">
|
||||||
<input type="text"
|
<input type="text" disabled value="{{ URL + "/oauth2/userinfo" }}" class="form-control">
|
||||||
disabled
|
|
||||||
value="{{ URL + "/oauth2/userinfo" }}"
|
|
||||||
class="form-control">
|
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button data-clipboard-text="{{ URL + "/oauth2/userinfo" }}"
|
<button data-clipboard-text="{{ URL + "/oauth2/userinfo" }}" class="clipboard btn btn-primary" type="button">
|
||||||
class="clipboard btn btn-primary"
|
|
||||||
type="button">
|
|
||||||
<i class="fe fe-clipboard"></i>
|
<i class="fe fe-clipboard"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<h1 class="h2">Referral</h1>
|
<h1 class="h2">Referral</h1>
|
||||||
<div>
|
<div>
|
||||||
If you are in the
|
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
|
program, you can attach a
|
||||||
referral to this website.
|
referral to this website.
|
||||||
Any SimpleLogin sign up thanks to the SIWSL on your website will be counted towards this referral.
|
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">
|
<select class="form-control" name="referral-id" id="client-select">
|
||||||
{% for referral in current_user.referrals %}
|
{% for referral in current_user.referrals %}
|
||||||
|
|
||||||
<option value="{{ referral.id }}"
|
<option value="{{ referral.id }}" {% if client.referral_id == referral.id %}selected{% endif %}>{{ referral.name }}</option>
|
||||||
{% if client.referral_id == referral.id %} selected{% endif %}>
|
|
||||||
{{ referral.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if client.referral_id is none %}
|
{% if client.referral_id is none %}<option value="" selected>No referral selected</option>{% endif %}
|
||||||
|
|
||||||
<option value="" selected>
|
|
||||||
No referral selected
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" class="btn btn-primary" value="Update">
|
<input type="submit" class="btn btn-primary" value="Update">
|
||||||
|
@ -18,9 +18,7 @@
|
|||||||
How to use <i class="fe fe-chevrons-down"></i>
|
How to use <i class="fe fe-chevrons-down"></i>
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="alert alert-primary collapse {% if not clients %} show{% endif %}"
|
<div class="alert alert-primary collapse {% if not clients %}show{% endif %}" id="howtouse" role="alert">
|
||||||
id="howtouse"
|
|
||||||
role="alert">
|
|
||||||
If you want to integrate SIWSL into your website,
|
If you want to integrate SIWSL into your website,
|
||||||
this page is for you.
|
this page is for you.
|
||||||
<br />
|
<br />
|
||||||
@ -28,14 +26,9 @@
|
|||||||
If you are using a CMS or any system that supports a OpenID Connect plugin, you can just point
|
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 👇
|
it to SimpleLogin OpenID Configuration endpoint 👇
|
||||||
<div class="input-group mt-2">
|
<div class="input-group mt-2">
|
||||||
<input type="text"
|
<input type="text" disabled value="{{ URL + "/.well-known/openid-configuration" }}" class="form-control">
|
||||||
disabled
|
|
||||||
value="{{ URL + "/.well-known/openid-configuration" }}"
|
|
||||||
class="form-control">
|
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}"
|
<button data-clipboard-text="{{ URL + "/.well-known/openid-configuration" }}" class="clipboard btn btn-primary" type="button">
|
||||||
class="clipboard btn btn-primary"
|
|
||||||
type="button">
|
|
||||||
<i class="fe fe-clipboard"></i>
|
<i class="fe fe-clipboard"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@ -46,7 +39,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group" role="group" aria-label="Basic example">
|
<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/"
|
<a href="https://simplelogin.io/docs/siwsl/app/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
<p style="font-size: 16px;
|
<p style="font-size: 16px;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
margin: .4em 0 1.1875em;">{{ text }}</p>
|
margin: .4em 0 1.1875em">{{ text }}</p>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
<!-- To be used instead of render_text, much better! -->
|
<!-- To be used instead of render_text, much better! -->
|
||||||
{% macro text() %}
|
{% macro text() %}
|
||||||
<p style="font-size: 16px;
|
<p style="font-size: 16px;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
margin: .4em 0 1.1875em;">{{ caller() }}</p>
|
margin: .4em 0 1.1875em">{{ caller() }}</p>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{% macro render_button(button_text, link) %}
|
{% macro render_button(button_text, link) %}
|
||||||
<!-- Action -->
|
<!-- Action -->
|
||||||
@ -25,12 +25,12 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 30px auto;
|
margin: 30px auto;
|
||||||
padding: 0;">
|
padding: 0">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"
|
<td align="center"
|
||||||
style="word-break: break-word;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;">
|
font-size: 16px">
|
||||||
<!-- Border based button
|
<!-- Border based button
|
||||||
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
||||||
<table width="100%"
|
<table width="100%"
|
||||||
@ -42,7 +42,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
|||||||
<td align="center"
|
<td align="center"
|
||||||
style="word-break: break-word;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;">
|
font-size: 16px">
|
||||||
<a href="{{ link }}"
|
<a href="{{ link }}"
|
||||||
class="f-fallback button"
|
class="f-fallback button"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -57,9 +57,7 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||||
-webkit-text-size-adjust: none;
|
-webkit-text-size-adjust: none;
|
||||||
box-sizing: border-box;">
|
box-sizing: border-box">{{ button_text }}</a>
|
||||||
{{ button_text }}
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@ -75,25 +73,23 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
|||||||
padding-top: 25px;
|
padding-top: 25px;
|
||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
border-top-color: #EAEAEC;
|
border-top-color: #EAEAEC;
|
||||||
border-top-style: solid;">
|
border-top-style: solid">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="word-break: break-word;
|
<td style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;">
|
font-size: 16px">
|
||||||
<p class="f-fallback sub"
|
<p class="f-fallback sub"
|
||||||
style="font-size: 13px;
|
style="font-size: 13px;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
margin: .4em 0 1.1875em;">
|
margin: .4em 0 1.1875em">
|
||||||
If you’re having trouble with the button above, copy and paste the URL below into your web browser.
|
If you’re having trouble with the button above, copy and paste the URL below into your web browser.
|
||||||
</p>
|
</p>
|
||||||
<p class="f-fallback sub"
|
<p class="f-fallback sub"
|
||||||
style="font-size: 13px;
|
style="font-size: 13px;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
margin: .4em 0 1.1875em;">
|
margin: .4em 0 1.1875em">{{ link }}</p>
|
||||||
{{ link }}
|
|
||||||
</p>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@ -104,14 +100,14 @@ https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
|||||||
cellpadding="0"
|
cellpadding="0"
|
||||||
cellspacing="0"
|
cellspacing="0"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="margin: 0 0 21px;">
|
style="margin: 0 0 21px">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="attributes_content"
|
<td class="attributes_content"
|
||||||
style="word-break: break-word;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: #F4F4F7;
|
background-color: #F4F4F7;
|
||||||
padding: 16px;"
|
padding: 16px"
|
||||||
bgcolor="#F4F4F7">
|
bgcolor="#F4F4F7">
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
{% for part in parts %}
|
{% 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;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 0;">
|
padding: 0">
|
||||||
<div class="f-fallback">
|
<div class="f-fallback">
|
||||||
{{ part }}
|
{{ part }}
|
||||||
<br />
|
<br />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% from "_emailhelpers.html" import render_text, text, render_button, raw_url, grey_section, section %}
|
{% 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"
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
xmlns:v="urn:schemas-microsoft-com:vml"
|
xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<!--[if gte mso 15]>
|
<!--[if gte mso 15]>
|
||||||
<xml>
|
<xml>
|
||||||
<o:OfficeDocumentSettings>
|
<o:OfficeDocumentSettings>
|
||||||
<o:AllowPNG/>
|
<o:AllowPNG />
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
</o:OfficeDocumentSettings>
|
</o:OfficeDocumentSettings>
|
||||||
</xml>
|
</xml>
|
||||||
@ -504,7 +504,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;">
|
-webkit-text-size-adjust: 100%">
|
||||||
<!--[if !gte mso 9]><!----><span class="mcnPreviewText"
|
<!--[if !gte mso 9]><!----><span class="mcnPreviewText"
|
||||||
style="display:none;
|
style="display:none;
|
||||||
font-size:0px;
|
font-size:0px;
|
||||||
@ -514,7 +514,7 @@
|
|||||||
opacity:0;
|
opacity:0;
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
visibility:hidden;
|
visibility:hidden;
|
||||||
mso-hide:all;"></span><!--<![endif]-->
|
mso-hide:all"></span><!--<![endif]-->
|
||||||
<center>
|
<center>
|
||||||
<table align="center"
|
<table align="center"
|
||||||
border="0"
|
border="0"
|
||||||
@ -531,7 +531,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;">
|
width: 100%">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"
|
<td align="center"
|
||||||
valign="top"
|
valign="top"
|
||||||
@ -542,7 +542,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
width: 100%;">
|
width: 100%">
|
||||||
<!-- BEGIN TEMPLATE // -->
|
<!-- BEGIN TEMPLATE // -->
|
||||||
<!--[if (gte mso 9)|(IE)]>
|
<!--[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;">
|
<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;
|
mso-table-rspace: 0pt;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
max-width: 600px !important;">
|
max-width: 600px !important">
|
||||||
<tr>
|
<tr>
|
||||||
<td valign="top"
|
<td valign="top"
|
||||||
id="templateHeader"
|
id="templateHeader"
|
||||||
style="mso-line-height-rule: exactly;
|
style="mso-line-height-rule: exactly;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;">
|
-webkit-text-size-adjust: 100%">
|
||||||
<table border="0"
|
<table border="0"
|
||||||
cellpadding="0"
|
cellpadding="0"
|
||||||
cellspacing="0"
|
cellspacing="0"
|
||||||
@ -574,14 +574,14 @@
|
|||||||
mso-table-lspace: 0pt;
|
mso-table-lspace: 0pt;
|
||||||
mso-table-rspace: 0pt;
|
mso-table-rspace: 0pt;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;">
|
-webkit-text-size-adjust: 100%">
|
||||||
<tbody class="mcnImageBlockOuter">
|
<tbody class="mcnImageBlockOuter">
|
||||||
<tr>
|
<tr>
|
||||||
<td valign="top"
|
<td valign="top"
|
||||||
style="padding: 16px;
|
style="padding: 16px;
|
||||||
mso-line-height-rule: exactly;
|
mso-line-height-rule: exactly;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;"
|
-webkit-text-size-adjust: 100%"
|
||||||
class="mcnImageBlockInner">
|
class="mcnImageBlockInner">
|
||||||
<table align="left"
|
<table align="left"
|
||||||
width="100%"
|
width="100%"
|
||||||
@ -594,7 +594,7 @@
|
|||||||
mso-table-lspace: 0pt;
|
mso-table-lspace: 0pt;
|
||||||
mso-table-rspace: 0pt;
|
mso-table-rspace: 0pt;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;">
|
-webkit-text-size-adjust: 100%">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="mcnImageContent"
|
<td class="mcnImageContent"
|
||||||
@ -603,13 +603,22 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
mso-line-height-rule: exactly;
|
mso-line-height-rule: exactly;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;">
|
-webkit-text-size-adjust: 100%">
|
||||||
<a href="https://proton.me/" target="_blank" style="">
|
<a href="https://proton.me/" target="_blank" style="">
|
||||||
<img align="center"
|
<img align="center"
|
||||||
alt="Proton"
|
alt="Proton"
|
||||||
src="{{ URL }}/static/logo-proton.png"
|
src="{{ URL }}/static/logo-proton.png"
|
||||||
width="190"
|
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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -624,7 +633,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td valign="top"
|
<td valign="top"
|
||||||
id="templateBody"
|
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 greeting %}{% endblock %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
<!-- Sub copy -->
|
<!-- Sub copy -->
|
||||||
|
@ -423,7 +423,7 @@
|
|||||||
.f-fallback {
|
.f-fallback {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<style type="text/css" rel="stylesheet" media="all">
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
body {
|
body {
|
||||||
@ -449,7 +449,7 @@
|
|||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
background-color: #F2F4F6;
|
background-color: #F2F4F6;
|
||||||
color: #51545E;
|
color: #51545E;
|
||||||
margin: 0;"
|
margin: 0"
|
||||||
bgcolor="#F2F4F6">
|
bgcolor="#F2F4F6">
|
||||||
<span class="preheader"
|
<span class="preheader"
|
||||||
style="display: none !important;
|
style="display: none !important;
|
||||||
@ -460,7 +460,7 @@
|
|||||||
max-height: 0;
|
max-height: 0;
|
||||||
max-width: 0;
|
max-width: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
overflow: hidden;">{{ pre_header }}</span>
|
overflow: hidden">{{ pre_header }}</span>
|
||||||
<table class="email-wrapper"
|
<table class="email-wrapper"
|
||||||
width="100%"
|
width="100%"
|
||||||
cellpadding="0"
|
cellpadding="0"
|
||||||
@ -472,13 +472,13 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
background-color: #F2F4F6;
|
background-color: #F2F4F6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;"
|
padding: 0"
|
||||||
bgcolor="#F2F4F6">
|
bgcolor="#F2F4F6">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"
|
<td align="center"
|
||||||
style="word-break: break-word;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;">
|
font-size: 16px">
|
||||||
<table class="email-content"
|
<table class="email-content"
|
||||||
width="100%"
|
width="100%"
|
||||||
cellpadding="0"
|
cellpadding="0"
|
||||||
@ -489,14 +489,14 @@
|
|||||||
-premailer-cellpadding: 0;
|
-premailer-cellpadding: 0;
|
||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;">
|
padding: 0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="email-masthead"
|
<td class="email-masthead"
|
||||||
style="word-break: break-word;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 25px 0;"
|
padding: 25px 0"
|
||||||
align="center">
|
align="center">
|
||||||
<a href="{{ LANDING_PAGE_URL }}"
|
<a href="{{ LANDING_PAGE_URL }}"
|
||||||
class="f-fallback email-masthead_name"
|
class="f-fallback email-masthead_name"
|
||||||
@ -504,7 +504,7 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: none;
|
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 %}
|
{% block logo %}<img src="{{ URL }}/static/logo.png" style="width: 150px; margin: auto">{% endblock %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@ -523,7 +523,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
-premailer-width: 100%;
|
-premailer-width: 100%;
|
||||||
-premailer-cellpadding: 0;
|
-premailer-cellpadding: 0;
|
||||||
-premailer-cellspacing: 0;">
|
-premailer-cellspacing: 0">
|
||||||
<table class="email-body_inner"
|
<table class="email-body_inner"
|
||||||
align="center"
|
align="center"
|
||||||
width="750"
|
width="750"
|
||||||
@ -536,7 +536,7 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;"
|
padding: 0"
|
||||||
bgcolor="#FFFFFF">
|
bgcolor="#FFFFFF">
|
||||||
<!-- Body content -->
|
<!-- Body content -->
|
||||||
<tr>
|
<tr>
|
||||||
@ -544,7 +544,7 @@
|
|||||||
style="word-break: break-word;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 30px;">
|
padding: 30px">
|
||||||
<div class="f-fallback">
|
<div class="f-fallback">
|
||||||
{% block greeting %}{% endblock %}
|
{% block greeting %}{% endblock %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
@ -559,7 +559,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="word-break: break-word;
|
<td style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;">
|
font-size: 16px">
|
||||||
<table class="email-footer"
|
<table class="email-footer"
|
||||||
align="center"
|
align="center"
|
||||||
width="750"
|
width="750"
|
||||||
@ -572,20 +572,20 @@
|
|||||||
-premailer-cellspacing: 0;
|
-premailer-cellspacing: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;">
|
padding: 0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="content-cell"
|
<td class="content-cell"
|
||||||
align="center"
|
align="center"
|
||||||
style="word-break: break-word;
|
style="word-break: break-word;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 30px;">
|
padding: 30px">
|
||||||
<p class="f-fallback sub align-center"
|
<p class="f-fallback sub align-center"
|
||||||
style="font-size: 13px;
|
style="font-size: 13px;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #A8AAAF;
|
color: #A8AAAF;
|
||||||
margin: .4em 0 1.1875em;"
|
margin: .4em 0 1.1875em"
|
||||||
align="center">
|
align="center">
|
||||||
© {{ YEAR }} SimpleLogin - a Proton product. All rights reserved.
|
© {{ YEAR }} SimpleLogin - a Proton product. All rights reserved.
|
||||||
<br />
|
<br />
|
||||||
@ -597,7 +597,7 @@
|
|||||||
style="font-size: 13px;
|
style="font-size: 13px;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: .4em 0 1.1875em;">
|
margin: .4em 0 1.1875em">
|
||||||
<a href="{{ unsubscribe_oneclick }}">Unsubscribe from our newsletter</a>
|
<a href="{{ unsubscribe_oneclick }}">Unsubscribe from our newsletter</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -606,7 +606,7 @@
|
|||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #A8AAAF;
|
color: #A8AAAF;
|
||||||
margin: .4em 0 1.1875em;"
|
margin: .4em 0 1.1875em"
|
||||||
align="center">
|
align="center">
|
||||||
<a href="https://app.simplelogin.io/dashboard/support">Do you have a question?</a>
|
<a href="https://app.simplelogin.io/dashboard/support">Do you have a question?</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
{{ render_text("Hi") }}
|
{{ render_text("Hi") }}
|
||||||
{{ render_text("Our most requested feature is finally ready: you can now add several <b>real</b> email addresses into SimpleLogin
|
{{ render_text("Our most requested feature is finally ready: you can now add several <b>real</b> email addresses into SimpleLogin
|
||||||
and choose which one to use when creating aliases!") }}
|
and choose which one to use when creating aliases!") }}
|
||||||
{{ render_text("A real email address is called <b>mailbox</b> in SimpleLogin.") }}
|
{{ render_text("A real email address is called <b>mailbox</b> in SimpleLogin.") }}
|
||||||
{{ render_text('This feature is particularly useful if you have several email addresses,
|
{{ render_text('This feature is particularly useful if you have several email addresses,
|
||||||
maybe for different uses: a Gmail account for social networks & forums, a Prontonmail account for professional emails, etc.') }}
|
maybe for different uses: a Gmail account for social networks & forums, a Prontonmail account for professional emails, etc.') }}
|
||||||
<img src="https://simplelogin.io/blog/mailbox-gmail.png"
|
<img src="https://simplelogin.io/blog/mailbox-gmail.png"
|
||||||
alt="Mailbox Gmail">
|
alt="Mailbox Gmail">
|
||||||
<img src="https://simplelogin.io/blog/mailbox-protonmail.png"
|
<img src="https://simplelogin.io/blog/mailbox-protonmail.png"
|
||||||
@ -18,9 +18,9 @@
|
|||||||
{{ render_text("You can also change the owning mailbox for an existing alias.") }}
|
{{ render_text("You can also change the owning mailbox for an existing alias.") }}
|
||||||
{{ render_text("The mailbox doesn't have to be your personal email: you can also create aliases for your friend by adding his/her email as a mailbox.") }}
|
{{ render_text("The mailbox doesn't have to be your personal email: you can also create aliases for your friend by adding his/her email as a mailbox.") }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
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 %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
|
||||||
|
@ -4,127 +4,127 @@
|
|||||||
|
|
||||||
{{ render_text("Hi") }}
|
{{ render_text("Hi") }}
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Son from SimpleLogin here. I hope you are doing well and are staying at home in this difficult time. By the way I'm
|
Son from SimpleLogin here. I hope you are doing well and are staying at home in this difficult time. By the way I'm
|
||||||
writing this newsletter from my couch with my cats proofreading the text :).
|
writing this newsletter from my couch with my cats proofreading the text :).
|
||||||
<br />
|
<br />
|
||||||
Please find below some of our latest news.
|
Please find below some of our latest news.
|
||||||
<br />
|
<br />
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
1) <b>Mobile apps</b>
|
1) <b>Mobile apps</b>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<img src="https://simplelogin.io/blog/devices.png" style="max-width: 100%">
|
<img src="https://simplelogin.io/blog/devices.png" style="max-width: 100%">
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Now you can quickly create aliases on-the-go with SimpleLogin Android and iOS app,
|
Now you can quickly create aliases on-the-go with SimpleLogin Android and iOS app,
|
||||||
thanks to our mobile guy Thanh-Nhon!
|
thanks to our mobile guy Thanh-Nhon!
|
||||||
<br />
|
<br />
|
||||||
Download the Android app on
|
Download the Android app on
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Play Store</a>
|
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Play Store</a>
|
||||||
and the iOS app on
|
and the iOS app on
|
||||||
<a href="https://apps.apple.com/app/id1494359858">App Store</a>
|
<a href="https://apps.apple.com/app/id1494359858">App Store</a>
|
||||||
.
|
.
|
||||||
<br />
|
<br />
|
||||||
With the release of the mobile apps, SimpleLogin now covers most major platforms:
|
With the release of the mobile apps, SimpleLogin now covers most major platforms:
|
||||||
<br />
|
<br />
|
||||||
- Desktop with SimpleLogin web app or Chrome, Firefox and Safari extension
|
- Desktop with SimpleLogin web app or Chrome, Firefox and Safari extension
|
||||||
<br />
|
<br />
|
||||||
- Mobile with Android and iOS app
|
- Mobile with Android and iOS app
|
||||||
<br />
|
<br />
|
||||||
The code is of course open-source and available on our
|
The code is of course open-source and available on our
|
||||||
<a href="https://github.com/simple-login/">Github</a>
|
<a href="https://github.com/simple-login/">Github</a>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
2) <b>Dark mode</b>
|
2) <b>Dark mode</b>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<img src="https://simplelogin.io/blog/dark-mode.gif" style="width: 100%">
|
<img src="https://simplelogin.io/blog/dark-mode.gif" style="width: 100%">
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
You have asked for it and now the dark mode is finally available, kudos to Dung - our full-stack guy.
|
You have asked for it and now the dark mode is finally available, kudos to Dung - our full-stack guy.
|
||||||
<br />
|
<br />
|
||||||
You can finally enjoy using SimpleLogin in the dark.
|
You can finally enjoy using SimpleLogin in the dark.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
3) <b>Alias name, new UI, security page, new policy privacy</b>
|
3) <b>Alias name, new UI, security page, new policy privacy</b>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<img src="https://simplelogin.io/blog/new-ui.gif" style="width: 100%">
|
<img src="https://simplelogin.io/blog/new-ui.gif" style="width: 100%">
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
You might have noticed that the web UI is now more compact: the web app has undergone a remake
|
You might have noticed that the web UI is now more compact: the web app has undergone a remake
|
||||||
to make it more responsive for usual actions like enabling/disabling an alias, updating alias note, etc.
|
to make it more responsive for usual actions like enabling/disabling an alias, updating alias note, etc.
|
||||||
<br />
|
<br />
|
||||||
You can set a name for your alias too: this name is used when you send emails or reply from your alias.
|
You can set a name for your alias too: this name is used when you send emails or reply from your alias.
|
||||||
<br />
|
<br />
|
||||||
We have also created a new
|
We have also created a new
|
||||||
<a href="https://simplelogin.io/security/">security page</a>
|
<a href="https://simplelogin.io/security/">security page</a>
|
||||||
that goes into the technical
|
that goes into the technical
|
||||||
details of SimpleLogin.
|
details of SimpleLogin.
|
||||||
Our
|
Our
|
||||||
<a href="https://simplelogin.io/privacy/">privacy page</a>
|
<a href="https://simplelogin.io/privacy/">privacy page</a>
|
||||||
is also rewritten from scratch: nothing changes about
|
is also rewritten from scratch: nothing changes about
|
||||||
your data protection
|
your data protection
|
||||||
but the page is more clear and detailed now.
|
but the page is more clear and detailed now.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
4) <b>Facebook, Google, Github login deprecation</b>
|
4) <b>Facebook, Google, Github login deprecation</b>
|
||||||
<br />
|
<br />
|
||||||
We have decided to deprecate those social login options because of several reasons:
|
We have decided to deprecate those social login options because of several reasons:
|
||||||
<br />
|
<br />
|
||||||
- Privacy: every time you sign in using one of these methods, the respective company knows and
|
- Privacy: every time you sign in using one of these methods, the respective company knows and
|
||||||
we have no information on what they do with this data.
|
we have no information on what they do with this data.
|
||||||
<br />
|
<br />
|
||||||
- Not fully open-standard compatible: these platforms enjoy their monopolies and
|
- Not fully open-standard compatible: these platforms enjoy their monopolies and
|
||||||
don't play well with open standards like OAuth2/OpenID: in fact, implementations on mobile of these social login
|
don't play well with open standards like OAuth2/OpenID: in fact, implementations on mobile of these social login
|
||||||
require their SDK that we refuse to add because of privacy concern.
|
require their SDK that we refuse to add because of privacy concern.
|
||||||
<br />
|
<br />
|
||||||
- Uniform experiences for all users: to have these social login in our iOS app, we need to support "Sign in with
|
- Uniform experiences for all users: to have these social login in our iOS app, we need to support "Sign in with
|
||||||
Apple" that isn't broadly available for Android users.
|
Apple" that isn't broadly available for Android users.
|
||||||
Again, another big tech enjoying its monopoly.
|
Again, another big tech enjoying its monopoly.
|
||||||
<br />
|
<br />
|
||||||
If you happen to use one of these social login options, please create a password for your account on the
|
If you happen to use one of these social login options, please create a password for your account on the
|
||||||
<a href="{{ URL }}/dashboard/setting">Setting page</a>
|
<a href="{{ URL }}/dashboard/setting">Setting page</a>
|
||||||
<br />
|
<br />
|
||||||
You can still sign in using these social login until 2020-05-31. After this date, they will be removed.
|
You can still sign in using these social login until 2020-05-31. After this date, they will be removed.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
5) <b>WebAuthn (Beta)</b>
|
5) <b>WebAuthn (Beta)</b>
|
||||||
<br />
|
<br />
|
||||||
Thanks to Raymond, a user of SimpleLogin, the WebAuthn is now available in Beta.
|
Thanks to Raymond, a user of SimpleLogin, the WebAuthn is now available in Beta.
|
||||||
Please reply to this email if you want to try this out.
|
Please reply to this email if you want to try this out.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<hr style="margin: 10px;">
|
<hr style="margin: 10px;">
|
||||||
On behalf of the team, I want to say thank you to all users who have helped to improve SimpleLogin code
|
On behalf of the team, I want to say thank you to all users who have helped to improve SimpleLogin code
|
||||||
and even contribute important features.
|
and even contribute important features.
|
||||||
That means a lot to us as SimpleLogin is after all an open-source project.
|
That means a lot to us as SimpleLogin is after all an open-source project.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
That's all for today. If you want to follow all our latest features, you can follow our
|
That's all for today. If you want to follow all our latest features, you can follow our
|
||||||
<a href="https://twitter.com/simplelogin">Twitter</a>
|
<a href="https://twitter.com/simplelogin">Twitter</a>
|
||||||
or join our
|
or join our
|
||||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
||||||
or subscribe to our
|
or subscribe to our
|
||||||
<a href="https://feed43.com/simplelogin.xml">RSS feed</a>
|
<a href="https://feed43.com/simplelogin.xml">RSS feed</a>
|
||||||
.
|
.
|
||||||
<br />
|
<br />
|
||||||
Now back to coding :).
|
Now back to coding :).
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Best,
|
Best,
|
||||||
<br />
|
<br />
|
||||||
Son.
|
Son.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
{{ render_text("Hi") }}
|
{{ 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 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("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("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("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"
|
<img src="https://simplelogin.io/blog/without-pgp.png"
|
||||||
alt="Without PGP"
|
alt="Without PGP"
|
||||||
style="max-width: 100%">
|
style="max-width: 100%">
|
||||||
@ -18,13 +18,13 @@
|
|||||||
{{ render_text("You can create and manage your PGP keys when adding or editing your mailboxes. Check it out on your mailbox dashboard.") }}
|
{{ render_text("You can create and manage your PGP keys when adding or editing your mailboxes. Check it out on your mailbox dashboard.") }}
|
||||||
{{ render_button("Add your PGP key", URL ~ "/dashboard/mailbox") }}
|
{{ render_button("Add your PGP key", URL ~ "/dashboard/mailbox") }}
|
||||||
{{ render_text("Our next important feature is the coming of an iOS app. If you use iPhone or iPad want to help us testing out the app, please reply to this email so we can add you into the TestFlight program.
|
{{ render_text("Our next important feature is the coming of an iOS app. If you use iPhone or iPad want to help us testing out the app, please reply to this email so we can add you into the TestFlight program.
|
||||||
") }}
|
") }}
|
||||||
{{ render_text("For Android users, don't worry: the Android version is already in progress.
|
{{ render_text("For Android users, don't worry: the Android version is already in progress.
|
||||||
") }}
|
") }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
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 %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
line-height: 160%;
|
line-height: 160%;
|
||||||
padding-top: 25px;
|
padding-top: 25px;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
font-family: sans-serif;"
|
font-family: sans-serif"
|
||||||
class="paragraph">
|
class="paragraph">
|
||||||
This email is sent to {{ user.email }}.
|
This email is sent to {{ user.email }}.
|
||||||
Unsubscribe on
|
Unsubscribe on
|
||||||
@ -28,16 +28,16 @@
|
|||||||
{{ render_text("Hi") }}
|
{{ render_text("Hi") }}
|
||||||
{{ render_text("If you use Safari on a MacBook or iMac, you should check out our new Safari extension.") }}
|
{{ render_text("If you use Safari on a MacBook or iMac, you should check out our new Safari extension.") }}
|
||||||
{{ render_text('It can be installed on
|
{{ render_text('It can be installed on
|
||||||
<a href="https://apps.apple.com/app/id6475835429">App Store</a>
|
<a href="https://apps.apple.com/app/id6475835429">App Store</a>
|
||||||
. Its code is available on
|
. Its code is available on
|
||||||
<a href="https://github.com/simple-login/mac-app">GitHub</a>
|
<a href="https://github.com/simple-login/mac-app">GitHub</a>
|
||||||
.') }}
|
.') }}
|
||||||
{{ render_text('
|
{{ render_text('
|
||||||
<img src="https://static.simplelogin.io/safari-extension.png"
|
<img src="https://static.simplelogin.io/safari-extension.png"
|
||||||
style="max-width: 600px">
|
style="max-width: 600px">
|
||||||
') }}
|
') }}
|
||||||
{{ render_text('See our annoucement post for more information on this feature
|
{{ render_text('See our annoucement post for more information on this feature
|
||||||
<a href="https://simplelogin.io/blog/safari-extension/">Introducing Safari extension</a>
|
<a href="https://simplelogin.io/blog/safari-extension/">Introducing Safari extension</a>
|
||||||
.') }}
|
.') }}
|
||||||
{{ render_text("As usual, let me know if you have any question by replying to this email.") }}
|
{{ render_text("As usual, let me know if you have any question by replying to this email.") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,30 +3,30 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Download SimpleLogin browser extensions and mobile apps to create aliases on-the-fly.</h1>
|
<h1>Download SimpleLogin browser extensions and mobile apps to create aliases on-the-fly.</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
If you want to quickly create aliases <b>without</b> going to SimpleLogin website, you can do that with SimpleLogin
|
If you want to quickly create aliases <b>without</b> going to SimpleLogin website, you can do that with SimpleLogin
|
||||||
<a href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">Chrome</a>
|
<a href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">Chrome</a>
|
||||||
(or other Chromium-based browsers like Brave or Vivaldi),
|
(or other Chromium-based browsers like Brave or Vivaldi),
|
||||||
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox</a>
|
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox</a>
|
||||||
and
|
and
|
||||||
<a href="https://apps.apple.com/app/id6475835429 ">Safari</a>
|
<a href="https://apps.apple.com/app/id6475835429 ">Safari</a>
|
||||||
extension.
|
extension.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
You can also manage your aliases using SimpleLogin
|
You can also manage your aliases using SimpleLogin
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Android App</a>
|
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Android App</a>
|
||||||
or
|
or
|
||||||
<a href="https://apps.apple.com/app/id1494359858">iOS app</a>
|
<a href="https://apps.apple.com/app/id1494359858">iOS app</a>
|
||||||
.
|
.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
<img src="https://simplelogin.io/images/everywhere.png"
|
<img src="https://simplelogin.io/images/everywhere.png"
|
||||||
alt="Available Everywhere"
|
alt="Available Everywhere"
|
||||||
style="max-width: 100%;">
|
style="max-width: 100%">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
|
||||||
|
@ -3,32 +3,34 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Add other mailboxes to SimpleLogin.</h1>
|
<h1>Add other mailboxes to SimpleLogin.</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
If you have several email inboxes, say Gmail and Proton Mail,
|
If you have several email inboxes, say Gmail and Proton Mail,
|
||||||
you can add them into SimpleLogin as <b>mailboxes</b>.
|
you can add them into SimpleLogin as <b>mailboxes</b>.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
<img src="https://simplelogin.io/images/multiple-mailboxes.png"
|
<img src="https://simplelogin.io/images/multiple-mailboxes.png"
|
||||||
alt="Multiple Mailboxes"
|
alt="Multiple Mailboxes"
|
||||||
style="max-width: 100%; margin: auto; border: 1px solid">
|
style="max-width: 100%;
|
||||||
{% call text() %}
|
margin: auto;
|
||||||
When creating an alias, you can choose the mailbox(es) that
|
border: 1px solid">
|
||||||
<b>owns</b> this alias, meaning:
|
{% call text() %}
|
||||||
<br />
|
When creating an alias, you can choose the mailbox(es) that
|
||||||
1. Emails sent to this alias are forwarded to the owning mailbox(es).
|
<b>owns</b> this alias, meaning:
|
||||||
<br />
|
<br />
|
||||||
2. The owning mailbox(es) can send emails from this alias.
|
1. Emails sent to this alias are forwarded to the owning mailbox(es).
|
||||||
{% endcall %}
|
<br />
|
||||||
|
2. The owning mailbox(es) can send emails from this alias.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Please note that adding additional mailboxes is only available in the Premium plan.
|
Please note that adding additional mailboxes is only available in the Premium plan.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_button("Create mailbox", URL ~ "/dashboard/mailbox") }}
|
{{ render_button("Create mailbox", URL ~ "/dashboard/mailbox") }}
|
||||||
{{ raw_url(URL ~ "/dashboard/mailbox") }}
|
{{ raw_url(URL ~ "/dashboard/mailbox") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
|
||||||
|
@ -3,32 +3,34 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Secure your emails with PGP.</h1>
|
<h1>Secure your emails with PGP.</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
If you use Gmail, Yahoo, Outlook, etc, you might want to use
|
If you use Gmail, Yahoo, Outlook, etc, you might want to use
|
||||||
<a href="https://en.wikipedia.org/wiki/Pretty_Good_Privacy">PGP</a>
|
<a href="https://en.wikipedia.org/wiki/Pretty_Good_Privacy">PGP</a>
|
||||||
(Pretty Good Privacy)
|
(Pretty Good Privacy)
|
||||||
to make sure your emails can't be read by these email providers.
|
to make sure your emails can't be read by these email providers.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Without PGP, emails are stored <b>in plaintext</b> leaving your email service able to read your emails.
|
Without PGP, emails are stored <b>in plaintext</b> leaving your email service able to read your emails.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
<img src="https://simplelogin.io/blog/without-pgp.png"
|
<img src="https://simplelogin.io/blog/without-pgp.png"
|
||||||
alt="Without PGP"
|
alt="Without PGP"
|
||||||
style="max-width: 100%; margin-bottom: 10px">
|
style="max-width: 100%;
|
||||||
{% call text() %}
|
margin-bottom: 10px">
|
||||||
With PGP enabled, SimpleLogin <b>encrypts</b> your emails with your public key before forwarding to your mailbox.
|
{% call text() %}
|
||||||
{% endcall %}
|
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"
|
<img src="https://simplelogin.io/blog/with-pgp.png"
|
||||||
alt="Without PGP"
|
alt="Without PGP"
|
||||||
style="max-width: 100%; margin-bottom: 20px">
|
style="max-width: 100%;
|
||||||
{{ render_button("Enable PGP on your mailbox", URL ~ "/dashboard/mailbox/" ~ user.default_mailbox_id) }}
|
margin-bottom: 20px">
|
||||||
{{ raw_url(URL ~ "/dashboard/mailbox/" ~ user.default_mailbox_id) }}
|
{{ render_button("Enable PGP on your mailbox", URL ~ "/dashboard/mailbox/" ~ user.default_mailbox_id) }}
|
||||||
|
{{ raw_url(URL ~ "/dashboard/mailbox/" ~ user.default_mailbox_id) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
|
||||||
|
@ -3,46 +3,46 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Send emails from your alias.</h1>
|
<h1>Send emails from your alias.</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
If you want to reply to an email, just hit "Reply"
|
If you want to reply to an email, just hit "Reply"
|
||||||
and the response will come from your alias. Your personal email address stays hidden.
|
and the response will come from your alias. Your personal email address stays hidden.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
To send an email to a <b>new contact</b>, please follow the steps below.
|
To send an email to a <b>new contact</b>, please follow the steps below.
|
||||||
You can also watch this
|
You can also watch this
|
||||||
<a href="https://youtu.be/GN060XMt6Pc">Youtube video</a>
|
<a href="https://youtu.be/GN060XMt6Pc">Youtube video</a>
|
||||||
that quickly walks you through the steps.
|
that quickly walks you through the steps.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
1. Click the <b>Contacts</b> button on the alias you want to send emails from
|
1. Click the <b>Contacts</b> button on the alias you want to send emails from
|
||||||
<br />
|
<br />
|
||||||
<img src="https://simplelogin.io/docs/getting-started/send-email/contacts.png"
|
<img src="https://simplelogin.io/docs/getting-started/send-email/contacts.png"
|
||||||
style="max-width: 500px">
|
style="max-width: 500px">
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
2. Enter your contact email, this will create a <b>reverse-alias</b> for the contact.
|
2. Enter your contact email, this will create a <b>reverse-alias</b> for the contact.
|
||||||
<br />
|
<br />
|
||||||
<img src="https://simplelogin.io/docs/getting-started/send-email/new-contact.png"
|
<img src="https://simplelogin.io/docs/getting-started/send-email/new-contact.png"
|
||||||
style="max-width: 500px">
|
style="max-width: 500px">
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
3. Send the email to this reverse-alias <b>instead of the contact email</b>.
|
3. Send the email to this reverse-alias <b>instead of the contact email</b>.
|
||||||
<br />
|
<br />
|
||||||
<img src="https://simplelogin.io/docs/getting-started/send-email/reverse-alias.png"
|
<img src="https://simplelogin.io/docs/getting-started/send-email/reverse-alias.png"
|
||||||
style="max-width: 500px">
|
style="max-width: 500px">
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
And voilà, your contact will receive this email sent from your alias!
|
And voilà, your contact will receive this email sent from your alias!
|
||||||
Your real mailbox address will stay hidden.
|
Your real mailbox address will stay hidden.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
@ -4,58 +4,57 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Welcome to SimpleLogin, a service developed by Proton to protect your email address!
|
Welcome to SimpleLogin, a service developed by Proton to protect your email address!
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
This is the first email you receive via your <b>first alias</b> {{ to_address }}
|
This is the first email you receive via your <b>first alias</b> {{ to_address }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
This alias is automatically created when you use SimpleLogin for the first time.
|
This alias is automatically created when you use SimpleLogin for the first time.
|
||||||
Emails sent to it are forwarded to your Proton mailbox.
|
Emails sent to it are forwarded to your Proton mailbox.
|
||||||
If you want to reply to an email, just hit "Reply" and the response will come from your alias.
|
If you want to reply to an email, just hit "Reply" and the response will come from your alias.
|
||||||
Your personal email address stays hidden.
|
Your personal email address stays hidden.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
To create new aliases, use the SimpleLogin browser extension (recommended) or web dashboard.
|
To create new aliases, use the SimpleLogin browser extension (recommended) or web dashboard.
|
||||||
SimpleLogin is available on
|
SimpleLogin is available on
|
||||||
<a href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">Chrome</a>
|
<a href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">Chrome</a>
|
||||||
,
|
,
|
||||||
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox</a>
|
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox</a>
|
||||||
and
|
and
|
||||||
<a href="https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff">
|
<a href="https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff">
|
||||||
Edge
|
Edge
|
||||||
</a>
|
</a>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
SimpleLogin is also available on
|
SimpleLogin is also available on
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Android</a>
|
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Android</a>
|
||||||
and
|
and
|
||||||
<a href="https://apps.apple.com/app/id1494359858">iOS</a>
|
<a href="https://apps.apple.com/app/id1494359858">iOS</a>
|
||||||
so you can manage your aliases on the go.
|
so you can manage your aliases on the go.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Note, if you are a paying Proton Mail user, you automatically receive the premium version of SimpleLogin.
|
Note, if you are a paying Proton Mail user, you automatically receive the premium version of SimpleLogin.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
For any question or feedback, please join our <a href="https://forum.simplelogin.io/">official forum</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>.
|
||||||
If you want to request a feature, please submit it on our <a href="https://github.com/simple-login/app/discussions">GitHub repo</a>.
|
You can also join our
|
||||||
You can also join our
|
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
||||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
or follow our
|
||||||
or follow our
|
<a href="https://twitter.com/simple_login">Twitter</a>
|
||||||
<a href="https://twitter.com/simple_login">Twitter</a>
|
.
|
||||||
.
|
{% endcall %}
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Best,
|
Best,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.
|
SimpleLogin Team.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -27,8 +27,7 @@ Firefox: https://addons.mozilla.org/firefox/addon/simplelogin/
|
|||||||
Edge: https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff
|
Edge: https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff
|
||||||
Android: https://play.google.com/store/apps/details?id=io.simplelogin.android
|
Android: https://play.google.com/store/apps/details?id=io.simplelogin.android
|
||||||
iOS: https://apps.apple.com/app/id1494359858
|
iOS: https://apps.apple.com/app/id1494359858
|
||||||
Github repo: https://github.com/simple-login/app/discussions
|
Forum: https://github.com/simple-login/app/discussions
|
||||||
Official forum: https://forum.simplelogin.io/
|
|
||||||
Reddit: https://www.reddit.com/r/Simplelogin/
|
Reddit: https://www.reddit.com/r/Simplelogin/
|
||||||
Twitter: https://twitter.com/simple_login
|
Twitter: https://twitter.com/simple_login
|
||||||
|
|
||||||
|
@ -6,80 +6,78 @@
|
|||||||
color: #333333;
|
color: #333333;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;"
|
text-align: left"
|
||||||
align="left">
|
align="left">Welcome!</h1>
|
||||||
Welcome!
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if alias %}
|
{% if alias %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
This is the first email you receive via your <b>first alias</b> <em>{{ alias }}</em>.
|
This is the first email you receive via your <b>first alias</b> <em>{{ alias }}</em>.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
This alias is automatically created for receiving SimpleLogin news and tips.
|
||||||
|
<br />
|
||||||
|
In the next coming days, we'll send you 3 emails to help you get the best out of SimpleLogin.
|
||||||
|
<br />
|
||||||
|
Please
|
||||||
|
<a href="{{ URL + '/dashboard/setting#notification' }}">disable</a>
|
||||||
|
it if you don't need this.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% call text() %}
|
||||||
|
If you are using Firefox or a Chromium-browser like Chrome, Edge, Brave, you can
|
||||||
|
install our
|
||||||
|
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox add-on</a>
|
||||||
|
or
|
||||||
|
<a href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">Chrome extension</a>
|
||||||
|
to create aliases in one click (like in the below gif 👇).
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
This alias is automatically created for receiving SimpleLogin news and tips.
|
<img src="https://simplelogin.io/images/one-click-alias.gif"
|
||||||
<br />
|
style="max-width: 80%;
|
||||||
In the next coming days, we'll send you 3 emails to help you get the best out of SimpleLogin.
|
margin: auto;
|
||||||
<br />
|
border: 1px solid">
|
||||||
Please
|
{% endcall %}
|
||||||
<a href="{{ URL + '/dashboard/setting#notification' }}">disable</a>
|
|
||||||
it if you don't need this.
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% call text() %}
|
|
||||||
If you are using Firefox or a Chromium-browser like Chrome, Edge, Brave, you can
|
|
||||||
install our
|
|
||||||
<a href="https://addons.mozilla.org/firefox/addon/simplelogin/">Firefox add-on</a>
|
|
||||||
or
|
|
||||||
<a href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">Chrome extension</a>
|
|
||||||
to create aliases in one click (like in the below gif 👇).
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% call text() %}
|
|
||||||
<img src="https://simplelogin.io/images/one-click-alias.gif"
|
|
||||||
style="max-width: 80%; margin: auto; border: 1px solid">
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% call text() %}
|
|
||||||
SimpleLogin is also available on
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Android</a>
|
|
||||||
and
|
|
||||||
<a href="https://apps.apple.com/app/id1494359858">iOS</a>
|
|
||||||
so you can manage your aliases on the go.
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% if user.in_trial() and user.trial_end %}
|
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
When you signed up, you can use all premium features like
|
SimpleLogin is also available on
|
||||||
<em>custom domain</em>, <em>alias directory</em>,
|
<a href="https://play.google.com/store/apps/details?id=io.simplelogin.android">Android</a>
|
||||||
<em>mailbox</em>,
|
and
|
||||||
<em>PGP</em> without any limit during 7 days (the "trial period").
|
<a href="https://apps.apple.com/app/id1494359858">iOS</a>
|
||||||
Everything you create during this period will
|
so you can manage your aliases on the go.
|
||||||
continue to work normally even if you don't upgrade.
|
{% endcall %}
|
||||||
<br />
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% call text() %}
|
{% if user.in_trial() and user.trial_end %}
|
||||||
Please note that you can't create more than {{ MAX_NB_EMAIL_FREE_PLAN }} aliases during the trial period.
|
|
||||||
<br />
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% endif %}
|
{% call text() %}
|
||||||
{% call text() %}
|
When you signed up, you can use all premium features like
|
||||||
For any question or feedback,
|
<em>custom domain</em>, <em>alias directory</em>,
|
||||||
please join our <a href="https://forum.simplelogin.io/">official forum</a>.
|
<em>mailbox</em>,
|
||||||
If you want to request a feature,
|
<em>PGP</em> without any limit during 7 days (the "trial period").
|
||||||
please submit it on our <a href="https://github.com/simple-login/app/discussions">GitHub repo</a>.
|
Everything you create during this period will
|
||||||
You can also join our
|
continue to work normally even if you don't upgrade.
|
||||||
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
|
<br />
|
||||||
or follow our
|
{% endcall %}
|
||||||
<a href="https://twitter.com/simplelogin">Twitter</a>
|
|
||||||
.
|
{% call text() %}
|
||||||
{% endcall %}
|
Please note that you can't create more than {{ MAX_NB_EMAIL_FREE_PLAN }} aliases during the trial period.
|
||||||
|
<br />
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% call text() %}
|
||||||
|
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
|
||||||
|
<a href="https://twitter.com/simplelogin">Twitter</a>
|
||||||
|
.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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.
|
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/
|
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
|
||||||
|
|
||||||
If you want to request a feature, please submit it on our GitHub repo 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
|
You can also join our Reddit at https://www.reddit.com/r/Simplelogin/ follow our Twitter at https://twitter.com/simplelogin
|
||||||
|
@ -3,14 +3,14 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Your SimpleLogin account has been deleted successfully.</h1>
|
<h1>Your SimpleLogin account has been deleted successfully.</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Thank you for having used SimpleLogin.
|
Thank you for having used SimpleLogin.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_text('Best,
|
{{ render_text('Best,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{{ render_text("If it wasn't you, maybe someone entered your email by mistake. In this case you can ignore this mail.") }}
|
{{ render_text("If it wasn't you, maybe someone entered your email by mistake. In this case you can ignore this mail.") }}
|
||||||
{{ render_button("Verify email", activation_link) }}
|
{{ render_button("Verify email", activation_link) }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{{ raw_url(activation_link) }}
|
{{ raw_url(activation_link) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,17 +3,17 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>{{ alias.email }} has been transferred.</h1>
|
<h1>{{ alias.email }} has been transferred.</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Your (previously) alias {{ alias.email }} has been received by another user.
|
Your (previously) alias {{ alias.email }} has been received by another user.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Best,
|
Best,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.
|
SimpleLogin Team.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,22 +3,22 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>{{ alias.email }} is disabled</h1>
|
<h1>{{ alias.email }} is disabled</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
There are several emails sent to your alias {{ alias.email }} that have been bounced
|
There are several emails sent to your alias {{ alias.email }} that have been bounced
|
||||||
by your mailbox {{ mailbox_email }}.
|
by your mailbox {{ mailbox_email }}.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_button("View the refused email", refused_email_url) }}
|
{{ render_button("View the refused email", refused_email_url) }}
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
As security measure, we have disabled the alias.
|
As security measure, we have disabled the alias.
|
||||||
{% endcall %}
|
{% 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,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{{ raw_url(refused_email_url) }}
|
{{ raw_url(refused_email_url) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,27 +3,27 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Email cannot be sent to {{ contact.email }} from your alias {{ alias.email }}</h1>
|
<h1>Email cannot be sent to {{ contact.email }} from your alias {{ alias.email }}</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
This might mean {{ contact.email }}
|
This might mean {{ contact.email }}
|
||||||
<ul>
|
<ul>
|
||||||
<li>is not a valid email address, or</li>
|
<li>is not a valid email address, or</li>
|
||||||
<li>doesn't exist, or</li>
|
<li>doesn't exist, or</li>
|
||||||
<li>its mail server refuses your email</li>
|
<li>its mail server refuses your email</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_button("View the original email", refused_email_url) }}
|
{{ render_button("View the original email", refused_email_url) }}
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
This email is automatically deleted in 7 days.
|
This email is automatically deleted in 7 days.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Best,
|
Best,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.
|
SimpleLogin Team.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,42 +3,42 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>
|
<h1>
|
||||||
An email sent to your alias {{ alias.email }} from {{ website_email }} was <b>bounced</b> by your mailbox
|
An email sent to your alias {{ alias.email }} from {{ website_email }} was <b>bounced</b> by your mailbox
|
||||||
{{ mailbox_email }}
|
{{ mailbox_email }}
|
||||||
</h1>
|
</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
This is usually because your mailbox service thinks the email is <b>spam</b>.
|
This is usually because your mailbox service thinks the email is <b>spam</b>.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_button("View the bounced email", refused_email_url) }}
|
{{ 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() %}
|
{% call text() %}
|
||||||
Please consider the following options:
|
Please consider the following options:
|
||||||
<br />
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
If the email is not spam, you can create a
|
|
||||||
<a href="https://simplelogin.io/docs/getting-started/troubleshooting/">filter</a>
|
|
||||||
to explicitly allow all emails from SimpleLogin.
|
|
||||||
<br />
|
<br />
|
||||||
</li>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
If this email is indeed spam, it means your alias {{ alias.email }} is now in the hands of a spammer.
|
If the email is not spam, you can create a
|
||||||
You can either
|
<a href="https://simplelogin.io/docs/getting-started/troubleshooting/">filter</a>
|
||||||
<a href="{{ disable_alias_link }}">disable the alias</a>
|
to explicitly allow all emails from SimpleLogin.
|
||||||
or
|
<br />
|
||||||
<a href="{{ block_sender_link }}">block the sender</a>
|
</li>
|
||||||
if they send too many spam emails.
|
<li>
|
||||||
</li>
|
If this email is indeed spam, it means your alias {{ alias.email }} is now in the hands of a spammer.
|
||||||
</ol>
|
You can either
|
||||||
<br />
|
<a href="{{ disable_alias_link }}">disable the alias</a>
|
||||||
Please note that the alias can be automatically disabled if too many emails sent to it are bounced.
|
or
|
||||||
{% endcall %}
|
<a href="{{ block_sender_link }}">block the sender</a>
|
||||||
|
if they send too many spam emails.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<br />
|
||||||
|
Please note that the alias can be automatically disabled if too many emails sent to it are bounced.
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,22 +3,22 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Cannot create {{ alias }} on-the-fly</h1>
|
<h1>Cannot create {{ alias }} on-the-fly</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call 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>
|
An email has been sent to the alias <b>{{ alias }}</b> that would be created automatically as you own the directory <b>{{ directory }}</b>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
As <b>on-the-fly alias creation is disabled</b> on this directory, the alias isn't created.
|
As <b>on-the-fly alias creation is disabled</b> on this directory, the alias isn't created.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
If you aren't aware of this alias, that probably means someone has discovered about your directory and is abusing it.
|
If you aren't aware of this alias, that probably means someone has discovered about your directory and is abusing it.
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Cannot create alias {{ alias }} on-the-fly</h1>
|
<h1>Cannot create alias {{ alias }} on-the-fly</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ 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("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("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,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,13 +3,13 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Cannot create {{ alias }} on-the-fly</h1>
|
<h1>Cannot create {{ alias }} on-the-fly</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ 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("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("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,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{{ render_text("Use the button below to confirm within the next 12 hours.") }}
|
{{ render_text("Use the button below to confirm within the next 12 hours.") }}
|
||||||
{{ render_button("Change email", link) }}
|
{{ render_button("Change email", link) }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{{ raw_url(link) }}
|
{{ raw_url(link) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
{{ render_text("Thank you for choosing SimpleLogin.") }}
|
{{ render_text("Thank you for choosing SimpleLogin.") }}
|
||||||
{{ render_text("To get started, please activate your account by entering the following code into the application:") }}
|
{{ render_text("To get started, please activate your account by entering the following code into the application:") }}
|
||||||
{{ render_text("
|
{{ render_text("
|
||||||
<h1>" + code + "</h1>
|
<h1>" + code + "</h1>
|
||||||
")}}
|
") }}
|
||||||
{{ render_text('Thanks,
|
{{ render_text('Thanks,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,21 +3,21 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
<h1>Your subscription has been extended!</h1>
|
<h1>Your subscription has been extended!</h1>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Your payment with cryptocurrency has been successfully processed.
|
Your payment with cryptocurrency has been successfully processed.
|
||||||
<br />
|
<br />
|
||||||
Your subscription has been extended to
|
Your subscription has been extended to
|
||||||
<b>{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}</b>
|
<b>{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}</b>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call text() %}
|
{% call text() %}
|
||||||
Thank you a lot for your support!
|
Thank you a lot for your support!
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{{ render_text('Best,
|
{{ render_text('Best,
|
||||||
<br />
|
<br />
|
||||||
SimpleLogin Team.') }}
|
SimpleLogin Team.') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user