Merge remote-tracking branch 'origin/master' into ac-store-contact-bounces
* origin/master: (29 commits) PR comments support "enabled" param in /api/v2/aliases Update PGPy to 0.5.4 to allow for python 3.10 Also install libpq-dev Fix python 3.10 Add methods to check if alias will be auto-created PR comments Allow sending messages in a background thread Use the proper import for newrelic agent not send emails to inform about an alias can't be created to disabled user prevent disabled user from using the api make sure disabled user can't create new alias Put version version between " so it is 3.10 instead of 3.1 Add workflow for python 3.10 Remove it for all creds Do not send the transports to the js part since we have not stored them previously move help to menu on small screen only show the help button on desktop use another logo for mobile add new parameter disabled in /GET /api/v2/aliases ...
This commit is contained in:
commit
e62022f032
4
.flake8
4
.flake8
|
@ -6,7 +6,9 @@ extend-ignore =
|
|||
E203,
|
||||
E501,
|
||||
# Ignore "f-string is missing placeholders"
|
||||
F541
|
||||
F541,
|
||||
# allow bare except
|
||||
E722, B001
|
||||
exclude =
|
||||
.git,
|
||||
__pycache__,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
## code changes will send PR to following users
|
||||
* @acasajus @cquintana92 @nguyenkims
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.7]
|
||||
python-version: [3.7, "3.10"]
|
||||
|
||||
# service containers to run with `postgres-job`
|
||||
services:
|
||||
|
@ -59,6 +59,12 @@ jobs:
|
|||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
|
||||
- name: Install OS dependencies
|
||||
if: ${{ matrix.python-version }} == '3.10'
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y libre2-dev libpq-dev
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
run: poetry install --no-interaction --no-root
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import re
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from email_validator import validate_email, EmailNotValidError
|
||||
from sqlalchemy.exc import IntegrityError, DataError
|
||||
|
||||
from app.config import BOUNCE_PREFIX_FOR_REPLY_PHASE, BOUNCE_PREFIX, BOUNCE_SUFFIX
|
||||
from app.config import (
|
||||
BOUNCE_PREFIX_FOR_REPLY_PHASE,
|
||||
BOUNCE_PREFIX,
|
||||
BOUNCE_SUFFIX,
|
||||
VERP_PREFIX,
|
||||
)
|
||||
from app.db import Session
|
||||
from app.email_utils import (
|
||||
get_email_domain_part,
|
||||
send_cannot_create_directory_alias,
|
||||
send_cannot_create_domain_alias,
|
||||
can_create_directory_for_address,
|
||||
send_cannot_create_directory_alias_disabled,
|
||||
get_email_local_part,
|
||||
send_cannot_create_domain_alias,
|
||||
)
|
||||
from app.errors import AliasInTrashError
|
||||
from app.log import LOG
|
||||
|
@ -27,10 +32,132 @@ from app.models import (
|
|||
Mailbox,
|
||||
EmailLog,
|
||||
Contact,
|
||||
AutoCreateRule,
|
||||
)
|
||||
from app.regex_utils import regex_match
|
||||
|
||||
|
||||
def get_user_if_alias_would_auto_create(
|
||||
address: str, notify_user: bool = False
|
||||
) -> Optional[User]:
|
||||
banned_prefix = f"{VERP_PREFIX}."
|
||||
if address.startswith(banned_prefix):
|
||||
LOG.w("alias %s can't start with %s", address, banned_prefix)
|
||||
return None
|
||||
|
||||
try:
|
||||
# Prevent addresses with unicode characters (🤯) in them for now.
|
||||
validate_email(address, check_deliverability=False, allow_smtputf8=False)
|
||||
except EmailNotValidError:
|
||||
return None
|
||||
|
||||
domain_and_rule = check_if_alias_can_be_auto_created_for_custom_domain(
|
||||
address, notify_user=notify_user
|
||||
)
|
||||
if domain_and_rule:
|
||||
return domain_and_rule[0].user
|
||||
directory = check_if_alias_can_be_auto_created_for_a_directory(
|
||||
address, notify_user=notify_user
|
||||
)
|
||||
if directory:
|
||||
return directory.user
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def check_if_alias_can_be_auto_created_for_custom_domain(
|
||||
address: str, notify_user: bool = True
|
||||
) -> Optional[Tuple[CustomDomain, Optional[AutoCreateRule]]]:
|
||||
"""
|
||||
Check if this address would generate an auto created alias.
|
||||
If that's the case return the domain that would create it and the rule that triggered it.
|
||||
If there's no rule it's a catchall creation
|
||||
"""
|
||||
alias_domain = get_email_domain_part(address)
|
||||
custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
|
||||
|
||||
if not custom_domain:
|
||||
return None
|
||||
|
||||
user: User = custom_domain.user
|
||||
if user.disabled:
|
||||
LOG.i("Disabled user %s can't create new alias via custom domain", user)
|
||||
return None
|
||||
|
||||
if not user.can_create_new_alias():
|
||||
if notify_user:
|
||||
send_cannot_create_domain_alias(custom_domain.user, address, alias_domain)
|
||||
return None
|
||||
|
||||
if not custom_domain.catch_all:
|
||||
if len(custom_domain.auto_create_rules) == 0:
|
||||
return None
|
||||
local = get_email_local_part(address)
|
||||
|
||||
for rule in custom_domain.auto_create_rules:
|
||||
if regex_match(rule.regex, local):
|
||||
LOG.d(
|
||||
"%s passes %s on %s",
|
||||
address,
|
||||
rule.regex,
|
||||
custom_domain,
|
||||
)
|
||||
return custom_domain, rule
|
||||
else: # no rule passes
|
||||
LOG.d("no rule passed to create %s", local)
|
||||
return None
|
||||
LOG.d("Create alias via catchall")
|
||||
|
||||
return custom_domain, None
|
||||
|
||||
|
||||
def check_if_alias_can_be_auto_created_for_a_directory(
|
||||
address: str, notify_user: bool = True
|
||||
) -> Optional[Directory]:
|
||||
"""
|
||||
Try to create an alias with directory
|
||||
If an alias would be created, return the dictionary that would trigger the creation. Otherwise, return None.
|
||||
"""
|
||||
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
|
||||
if not can_create_directory_for_address(address):
|
||||
return None
|
||||
|
||||
# alias contains one of the 3 special directory separator: "/", "+" or "#"
|
||||
if "/" in address:
|
||||
sep = "/"
|
||||
elif "+" in address:
|
||||
sep = "+"
|
||||
elif "#" in address:
|
||||
sep = "#"
|
||||
else:
|
||||
# if there's no directory separator in the alias, no way to auto-create it
|
||||
return None
|
||||
|
||||
directory_name = address[: address.find(sep)]
|
||||
LOG.d("directory_name %s", directory_name)
|
||||
|
||||
directory = Directory.get_by(name=directory_name)
|
||||
if not directory:
|
||||
return None
|
||||
|
||||
user: User = directory.user
|
||||
if user.disabled:
|
||||
LOG.i("Disabled %s can't create new alias with directory", user)
|
||||
return None
|
||||
|
||||
if not user.can_create_new_alias():
|
||||
if notify_user:
|
||||
send_cannot_create_directory_alias(user, address, directory_name)
|
||||
return None
|
||||
|
||||
if directory.disabled:
|
||||
if notify_user:
|
||||
send_cannot_create_directory_alias_disabled(user, address, directory_name)
|
||||
return None
|
||||
|
||||
return directory
|
||||
|
||||
|
||||
def try_auto_create(address: str) -> Optional[Alias]:
|
||||
"""Try to auto-create the alias using directory or catch-all domain"""
|
||||
# VERP for reply phase is {BOUNCE_PREFIX_FOR_REPLY_PHASE}+{email_log.id}+@{alias_domain}
|
||||
|
@ -60,118 +187,72 @@ def try_auto_create_directory(address: str) -> Optional[Alias]:
|
|||
"""
|
||||
Try to create an alias with directory
|
||||
"""
|
||||
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
|
||||
if can_create_directory_for_address(address):
|
||||
# if there's no directory separator in the alias, no way to auto-create it
|
||||
if "/" not in address and "+" not in address and "#" not in address:
|
||||
return None
|
||||
directory = check_if_alias_can_be_auto_created_for_a_directory(
|
||||
address, notify_user=True
|
||||
)
|
||||
if not directory:
|
||||
return None
|
||||
|
||||
# alias contains one of the 3 special directory separator: "/", "+" or "#"
|
||||
if "/" in address:
|
||||
sep = "/"
|
||||
elif "+" in address:
|
||||
sep = "+"
|
||||
else:
|
||||
sep = "#"
|
||||
try:
|
||||
LOG.d("create alias %s for directory %s", address, directory)
|
||||
|
||||
directory_name = address[: address.find(sep)]
|
||||
LOG.d("directory_name %s", directory_name)
|
||||
mailboxes = directory.mailboxes
|
||||
|
||||
directory = Directory.get_by(name=directory_name)
|
||||
if not directory:
|
||||
return None
|
||||
|
||||
user: User = directory.user
|
||||
|
||||
if not user.can_create_new_alias():
|
||||
send_cannot_create_directory_alias(user, address, directory_name)
|
||||
return None
|
||||
|
||||
if directory.disabled:
|
||||
send_cannot_create_directory_alias_disabled(user, address, directory_name)
|
||||
return None
|
||||
|
||||
try:
|
||||
LOG.d("create alias %s for directory %s", address, directory)
|
||||
|
||||
mailboxes = directory.mailboxes
|
||||
|
||||
alias = Alias.create(
|
||||
email=address,
|
||||
user_id=directory.user_id,
|
||||
directory_id=directory.id,
|
||||
mailbox_id=mailboxes[0].id,
|
||||
alias = Alias.create(
|
||||
email=address,
|
||||
user_id=directory.user_id,
|
||||
directory_id=directory.id,
|
||||
mailbox_id=mailboxes[0].id,
|
||||
)
|
||||
if not directory.user.disable_automatic_alias_note:
|
||||
alias.note = f"Created by directory {directory.name}"
|
||||
Session.flush()
|
||||
for i in range(1, len(mailboxes)):
|
||||
AliasMailbox.create(
|
||||
alias_id=alias.id,
|
||||
mailbox_id=mailboxes[i].id,
|
||||
)
|
||||
if not user.disable_automatic_alias_note:
|
||||
alias.note = f"Created by directory {directory.name}"
|
||||
Session.flush()
|
||||
for i in range(1, len(mailboxes)):
|
||||
AliasMailbox.create(
|
||||
alias_id=alias.id,
|
||||
mailbox_id=mailboxes[i].id,
|
||||
)
|
||||
|
||||
Session.commit()
|
||||
return alias
|
||||
except AliasInTrashError:
|
||||
LOG.w(
|
||||
"Alias %s was deleted before, cannot auto-create using directory %s, user %s",
|
||||
address,
|
||||
directory_name,
|
||||
user,
|
||||
)
|
||||
return None
|
||||
except IntegrityError:
|
||||
LOG.w("Alias %s already exists", address)
|
||||
Session.rollback()
|
||||
alias = Alias.get_by(email=address)
|
||||
return alias
|
||||
Session.commit()
|
||||
return alias
|
||||
except AliasInTrashError:
|
||||
LOG.w(
|
||||
"Alias %s was deleted before, cannot auto-create using directory %s, user %s",
|
||||
address,
|
||||
directory.name,
|
||||
directory.user,
|
||||
)
|
||||
return None
|
||||
except IntegrityError:
|
||||
LOG.w("Alias %s already exists", address)
|
||||
Session.rollback()
|
||||
alias = Alias.get_by(email=address)
|
||||
return alias
|
||||
|
||||
|
||||
def try_auto_create_via_domain(address: str) -> Optional[Alias]:
|
||||
"""Try to create an alias with catch-all or auto-create rules on custom domain"""
|
||||
|
||||
# try to create alias on-the-fly with custom-domain catch-all feature
|
||||
# check if alias is custom-domain alias and if the custom-domain has catch-all enabled
|
||||
alias_domain = get_email_domain_part(address)
|
||||
custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
|
||||
|
||||
if not custom_domain:
|
||||
can_create = check_if_alias_can_be_auto_created_for_custom_domain(address)
|
||||
if not can_create:
|
||||
return None
|
||||
custom_domain, rule = can_create
|
||||
|
||||
if not custom_domain.catch_all and len(custom_domain.auto_create_rules) == 0:
|
||||
return None
|
||||
elif not custom_domain.catch_all and len(custom_domain.auto_create_rules) > 0:
|
||||
local = get_email_local_part(address)
|
||||
|
||||
for rule in custom_domain.auto_create_rules:
|
||||
if regex_match(rule.regex, local):
|
||||
LOG.d(
|
||||
"%s passes %s on %s",
|
||||
address,
|
||||
rule.regex,
|
||||
custom_domain,
|
||||
)
|
||||
alias_note = f"Created by rule {rule.order} with regex {rule.regex}"
|
||||
mailboxes = rule.mailboxes
|
||||
break
|
||||
else: # no rule passes
|
||||
LOG.d("no rule passed to create %s", local)
|
||||
return
|
||||
else: # catch-all is enabled
|
||||
if rule:
|
||||
alias_note = f"Created by rule {rule.order} with regex {rule.regex}"
|
||||
mailboxes = rule.mailboxes
|
||||
else:
|
||||
alias_note = "Created by catchall option"
|
||||
mailboxes = custom_domain.mailboxes
|
||||
alias_note = "Created by catch-all option"
|
||||
|
||||
domain_user: User = custom_domain.user
|
||||
|
||||
if not domain_user.can_create_new_alias():
|
||||
send_cannot_create_domain_alias(domain_user, address, alias_domain)
|
||||
return None
|
||||
|
||||
# a rule can have 0 mailboxes. Happened when a mailbox is deleted
|
||||
if not mailboxes:
|
||||
LOG.d("use %s default mailbox for %s %s", domain_user, address, custom_domain)
|
||||
mailboxes = [domain_user.default_mailbox]
|
||||
LOG.d(
|
||||
"use %s default mailbox for %s %s",
|
||||
custom_domain.user,
|
||||
address,
|
||||
custom_domain,
|
||||
)
|
||||
mailboxes = [custom_domain.user.default_mailbox]
|
||||
|
||||
try:
|
||||
LOG.d("create alias %s for domain %s", address, custom_domain)
|
||||
|
@ -197,7 +278,7 @@ def try_auto_create_via_domain(address: str) -> Optional[Alias]:
|
|||
"Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s",
|
||||
address,
|
||||
custom_domain,
|
||||
domain_user,
|
||||
custom_domain.user,
|
||||
)
|
||||
return None
|
||||
except IntegrityError:
|
||||
|
|
|
@ -30,6 +30,9 @@ def require_api_auth(f):
|
|||
|
||||
g.user = api_key.user
|
||||
|
||||
if g.user.disabled:
|
||||
return jsonify(error="Disabled account"), 403
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
|
|
@ -79,6 +79,8 @@ def get_aliases_v2():
|
|||
Input:
|
||||
page_id: in query
|
||||
pinned: in query
|
||||
disabled: in query
|
||||
enabled: in query
|
||||
Output:
|
||||
- aliases: list of alias:
|
||||
- id
|
||||
|
@ -110,6 +112,17 @@ def get_aliases_v2():
|
|||
return jsonify(error="page_id must be provided in request query"), 400
|
||||
|
||||
pinned = "pinned" in request.args
|
||||
disabled = "disabled" in request.args
|
||||
enabled = "enabled" in request.args
|
||||
|
||||
if pinned:
|
||||
alias_filter = "pinned"
|
||||
elif disabled:
|
||||
alias_filter = "disabled"
|
||||
elif enabled:
|
||||
alias_filter = "enabled"
|
||||
else:
|
||||
alias_filter = None
|
||||
|
||||
query = None
|
||||
data = request.get_json(silent=True)
|
||||
|
@ -117,7 +130,7 @@ def get_aliases_v2():
|
|||
query = data.get("query")
|
||||
|
||||
alias_infos: [AliasInfo] = get_alias_infos_with_pagination_v3(
|
||||
user, page_id=page_id, query=query, alias_filter="pinned" if pinned else None
|
||||
user, page_id=page_id, query=query, alias_filter=alias_filter
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -153,6 +153,13 @@ def fido():
|
|||
webauthn_users, challenge
|
||||
)
|
||||
webauthn_assertion_options = webauthn_assertion_options.assertion_dict
|
||||
try:
|
||||
# HACK: We need to upgrade to webauthn > 1 so it can support specifying the transports
|
||||
for credential in webauthn_assertion_options["allowCredentials"]:
|
||||
del credential["transports"]
|
||||
except KeyError:
|
||||
# Should never happen but...
|
||||
pass
|
||||
|
||||
return render_template(
|
||||
"auth/fido.html",
|
||||
|
|
|
@ -5,14 +5,14 @@ from email.message import Message
|
|||
import aiospamc
|
||||
|
||||
from app.config import SPAMASSASSIN_HOST
|
||||
from app.email_utils import to_bytes
|
||||
from app.log import LOG
|
||||
from app.message_utils import message_to_bytes
|
||||
from app.models import EmailLog
|
||||
from app.spamassassin_utils import SpamAssassin
|
||||
|
||||
|
||||
async def get_spam_score_async(message: Message) -> float:
|
||||
sa_input = to_bytes(message)
|
||||
sa_input = message_to_bytes(message)
|
||||
|
||||
# Spamassassin requires to have an ending linebreak
|
||||
if not sa_input.endswith(b"\n"):
|
||||
|
@ -41,7 +41,7 @@ def get_spam_score(
|
|||
Return the spam score and spam report
|
||||
"""
|
||||
LOG.d("get spam score for %s", email_log)
|
||||
sa_input = to_bytes(message)
|
||||
sa_input = message_to_bytes(message)
|
||||
|
||||
# Spamassassin requires to have an ending linebreak
|
||||
if not sa_input.endswith(b"\n"):
|
||||
|
|
|
@ -8,23 +8,20 @@ import random
|
|||
import time
|
||||
import uuid
|
||||
from copy import deepcopy
|
||||
|
||||
from aiosmtpd.smtp import Envelope
|
||||
|
||||
from email import policy, message_from_bytes, message_from_string
|
||||
from email.header import decode_header, Header
|
||||
from email.message import Message, EmailMessage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import make_msgid, formatdate
|
||||
from smtplib import SMTP, SMTPServerDisconnected, SMTPException, SMTPRecipientsRefused
|
||||
from smtplib import SMTP, SMTPException
|
||||
from typing import Tuple, List, Optional, Union
|
||||
|
||||
import arrow
|
||||
import dkim
|
||||
import newrelic.agent
|
||||
import re2 as re
|
||||
import spf
|
||||
from aiosmtpd.smtp import Envelope
|
||||
from cachetools import cached, TTLCache
|
||||
from email_validator import (
|
||||
validate_email,
|
||||
|
@ -65,6 +62,8 @@ from app.db import Session
|
|||
from app.dns_utils import get_mx_domains
|
||||
from app.email import headers
|
||||
from app.log import LOG
|
||||
from app.mail_sender import sl_sendmail
|
||||
from app.message_utils import message_to_bytes
|
||||
from app.models import (
|
||||
Mailbox,
|
||||
User,
|
||||
|
@ -86,6 +85,10 @@ from app.utils import (
|
|||
sanitize_email,
|
||||
)
|
||||
|
||||
# 2022-01-01 00:00:00
|
||||
VERP_TIME_START = 1640995200
|
||||
VERP_HMAC_ALGO = "sha3-224"
|
||||
|
||||
|
||||
def render(template_name, **kwargs) -> str:
|
||||
templates_dir = os.path.join(ROOT_DIR, "templates", "emails")
|
||||
|
@ -475,7 +478,7 @@ def add_dkim_signature_with_header(
|
|||
# Generate message signature
|
||||
if DKIM_PRIVATE_KEY:
|
||||
sig = dkim.sign(
|
||||
to_bytes(msg),
|
||||
message_to_bytes(msg),
|
||||
DKIM_SELECTOR,
|
||||
email_domain.encode(),
|
||||
DKIM_PRIVATE_KEY.encode(),
|
||||
|
@ -810,29 +813,24 @@ def copy(msg: Message) -> Message:
|
|||
return message_from_string(msg.as_string())
|
||||
except (UnicodeEncodeError, LookupError):
|
||||
LOG.w("as_string() fails, try bytes parsing")
|
||||
return message_from_bytes(to_bytes(msg))
|
||||
return message_from_bytes(message_to_bytes(msg))
|
||||
|
||||
|
||||
def to_bytes(msg: Message):
|
||||
"""replace Message.as_bytes() method by trying different policies"""
|
||||
try:
|
||||
return msg.as_bytes()
|
||||
except UnicodeEncodeError:
|
||||
LOG.w("as_bytes fails with default policy, try SMTP policy")
|
||||
for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]:
|
||||
try:
|
||||
return msg.as_bytes(policy=policy.SMTP)
|
||||
except UnicodeEncodeError:
|
||||
LOG.w("as_bytes fails with SMTP policy, try SMTPUTF8 policy")
|
||||
try:
|
||||
return msg.as_bytes(policy=policy.SMTPUTF8)
|
||||
except UnicodeEncodeError:
|
||||
LOG.w("as_bytes fails with SMTPUTF8 policy, try converting to string")
|
||||
msg_string = msg.as_string()
|
||||
try:
|
||||
return msg_string.encode()
|
||||
except UnicodeEncodeError as e:
|
||||
LOG.w("can't encode msg, err:%s", e)
|
||||
return msg_string.encode(errors="replace")
|
||||
return msg.as_bytes(policy=generator_policy)
|
||||
except:
|
||||
LOG.w("as_bytes() fails with %s policy", policy, exc_info=True)
|
||||
|
||||
msg_string = msg.as_string()
|
||||
try:
|
||||
return msg_string.encode()
|
||||
except:
|
||||
LOG.w("as_string().encode() fails", exc_info=True)
|
||||
|
||||
return msg_string.encode(errors="replace")
|
||||
|
||||
|
||||
def should_add_dkim_signature(domain: str) -> bool:
|
||||
|
@ -1286,82 +1284,6 @@ def get_smtp_server():
|
|||
return smtp
|
||||
|
||||
|
||||
def sl_sendmail(
|
||||
from_addr,
|
||||
to_addr,
|
||||
msg: Message,
|
||||
mail_options=(),
|
||||
rcpt_options=(),
|
||||
is_forward: bool = False,
|
||||
retries=2,
|
||||
ignore_smtp_error=False,
|
||||
):
|
||||
"""replace smtp.sendmail"""
|
||||
if NOT_SEND_EMAIL:
|
||||
LOG.d(
|
||||
"send email with subject '%s', from '%s' to '%s'",
|
||||
msg[headers.SUBJECT],
|
||||
msg[headers.FROM],
|
||||
msg[headers.TO],
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
start = time.time()
|
||||
if POSTFIX_SUBMISSION_TLS:
|
||||
smtp_port = 587
|
||||
else:
|
||||
smtp_port = POSTFIX_PORT
|
||||
|
||||
with SMTP(POSTFIX_SERVER, smtp_port) as smtp:
|
||||
if POSTFIX_SUBMISSION_TLS:
|
||||
smtp.starttls()
|
||||
|
||||
elapsed = time.time() - start
|
||||
LOG.d("getting a smtp connection takes seconds %s", elapsed)
|
||||
newrelic.agent.record_custom_metric("Custom/smtp_connection_time", elapsed)
|
||||
|
||||
# smtp.send_message has UnicodeEncodeError
|
||||
# encode message raw directly instead
|
||||
LOG.d(
|
||||
"Sendmail mail_from:%s, rcpt_to:%s, header_from:%s, header_to:%s, header_cc:%s",
|
||||
from_addr,
|
||||
to_addr,
|
||||
msg[headers.FROM],
|
||||
msg[headers.TO],
|
||||
msg[headers.CC],
|
||||
)
|
||||
smtp.sendmail(
|
||||
from_addr,
|
||||
to_addr,
|
||||
to_bytes(msg),
|
||||
mail_options,
|
||||
rcpt_options,
|
||||
)
|
||||
except (SMTPServerDisconnected, SMTPRecipientsRefused) as e:
|
||||
if retries > 0:
|
||||
LOG.w(
|
||||
"SMTPServerDisconnected or SMTPRecipientsRefused error %s, retry",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
time.sleep(0.3 * retries)
|
||||
sl_sendmail(
|
||||
from_addr,
|
||||
to_addr,
|
||||
msg,
|
||||
mail_options,
|
||||
rcpt_options,
|
||||
is_forward,
|
||||
retries=retries - 1,
|
||||
)
|
||||
else:
|
||||
if ignore_smtp_error:
|
||||
LOG.w("Ignore smtp error %s", e)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def get_queue_id(msg: Message) -> Optional[str]:
|
||||
"""Get the Postfix queue-id from a message"""
|
||||
header_values = msg.get_all(headers.RSPAMD_QUEUE_ID)
|
||||
|
@ -1450,14 +1372,22 @@ def save_envelope_for_debugging(envelope: Envelope, file_name_prefix=None) -> st
|
|||
def generate_verp_email(
|
||||
verp_type: VerpType, object_id: int, sender_domain: Optional[str] = None
|
||||
) -> str:
|
||||
"""Generates an email address with the verp type, object_id and domain encoded in the address
|
||||
and signed with hmac to prevent tampering
|
||||
"""
|
||||
# Encoded as a list to minimize size of email address
|
||||
data = [verp_type.bounce_forward.value, object_id, int(time.time())]
|
||||
# Time is in minutes granularity and start counting on 2022-01-01 to reduce bytes to represent time
|
||||
data = [
|
||||
verp_type.bounce_forward.value,
|
||||
object_id,
|
||||
int((time.time() - VERP_TIME_START) / 60),
|
||||
]
|
||||
json_payload = json.dumps(data).encode("utf-8")
|
||||
# Signing without itsdangereous because it uses base64 that includes +/= symbols and lower and upper case letters.
|
||||
# We need to encode in base32
|
||||
payload_hmac = hmac.new(
|
||||
VERP_EMAIL_SECRET.encode("utf-8"), json_payload, "shake128"
|
||||
).digest()
|
||||
VERP_EMAIL_SECRET.encode("utf-8"), json_payload, VERP_HMAC_ALGO
|
||||
).digest()[:8]
|
||||
encoded_payload = base64.b32encode(json_payload).rstrip(b"=").decode("utf-8")
|
||||
encoded_signature = base64.b32encode(payload_hmac).rstrip(b"=").decode("utf-8")
|
||||
return "{}.{}.{}@{}".format(
|
||||
|
@ -1465,9 +1395,8 @@ def generate_verp_email(
|
|||
).lower()
|
||||
|
||||
|
||||
# This method processes the email address, checks if it's a signed verp email generated by us to receive bounces
|
||||
# and extracts the type of verp email and associated email log id/transactional email id stored as object_id
|
||||
def get_verp_info_from_email(email: str) -> Optional[Tuple[VerpType, int]]:
|
||||
# Remove this method after 2022-05-20. Just for backwards compat.
|
||||
def deprecated_get_verp_info_from_email(email: str) -> Optional[Tuple[VerpType, int]]:
|
||||
idx = email.find("@")
|
||||
if idx == -1:
|
||||
return None
|
||||
|
@ -1491,3 +1420,39 @@ def get_verp_info_from_email(email: str) -> Optional[Tuple[VerpType, int]]:
|
|||
if data[2] > time.time() + VERP_MESSAGE_LIFETIME:
|
||||
return None
|
||||
return VerpType(data[0]), data[1]
|
||||
|
||||
|
||||
def new_get_verp_info_from_email(email: str) -> Optional[Tuple[VerpType, int]]:
|
||||
"""This method processes the email address, checks if it's a signed verp email generated by us to receive bounces
|
||||
and extracts the type of verp email and associated email log id/transactional email id stored as object_id
|
||||
"""
|
||||
idx = email.find("@")
|
||||
if idx == -1:
|
||||
return None
|
||||
username = email[:idx]
|
||||
fields = username.split(".")
|
||||
if len(fields) != 3 or fields[0] != VERP_PREFIX:
|
||||
return None
|
||||
padding = (8 - (len(fields[1]) % 8)) % 8
|
||||
payload = base64.b32decode(fields[1].encode("utf-8").upper() + (b"=" * padding))
|
||||
padding = (8 - (len(fields[2]) % 8)) % 8
|
||||
signature = base64.b32decode(fields[2].encode("utf-8").upper() + (b"=" * padding))
|
||||
expected_signature = hmac.new(
|
||||
VERP_EMAIL_SECRET.encode("utf-8"), payload, VERP_HMAC_ALGO
|
||||
).digest()[:8]
|
||||
if expected_signature != signature:
|
||||
return None
|
||||
data = json.loads(payload)
|
||||
# verp type, object_id, time
|
||||
if len(data) != 3:
|
||||
return None
|
||||
if data[2] > (time.time() + VERP_MESSAGE_LIFETIME - VERP_TIME_START) / 60:
|
||||
return None
|
||||
return VerpType(data[0]), data[1]
|
||||
|
||||
|
||||
# Replace with new_get_verp_info_from_email when deprecated_get_verp_info_from_email is removed
|
||||
def get_verp_info_from_email(email: str) -> Optional[Tuple[VerpType, int]]:
|
||||
return new_get_verp_info_from_email(email) or deprecated_get_verp_info_from_email(
|
||||
email
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import newrelic
|
||||
import newrelic.agent
|
||||
|
||||
from app.models import EnumE
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@ from app.email_utils import (
|
|||
send_email_with_rate_control,
|
||||
render,
|
||||
add_or_replace_header,
|
||||
to_bytes,
|
||||
add_header,
|
||||
)
|
||||
from app.handler.spamd_result import SpamdResult, Phase, DmarcCheckResult
|
||||
from app.log import LOG
|
||||
from app.message_utils import message_to_bytes
|
||||
from app.models import Alias, Contact, Notification, EmailLog, RefusedEmail
|
||||
|
||||
|
||||
|
@ -102,7 +102,7 @@ def quarantine_dmarc_failed_forward_email(alias, contact, envelope, msg) -> Emai
|
|||
random_name = str(uuid.uuid4())
|
||||
s3_report_path = f"refused-emails/full-{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(
|
||||
s3_report_path, BytesIO(to_bytes(msg)), f"full-{random_name}"
|
||||
s3_report_path, BytesIO(message_to_bytes(msg)), f"full-{random_name}"
|
||||
)
|
||||
refused_email = RefusedEmail.create(
|
||||
full_report_path=s3_report_path, user_id=alias.user_id, flush=True
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
from typing import Dict, Optional
|
||||
|
||||
import newrelic
|
||||
import newrelic.agent
|
||||
|
||||
from app.email import headers
|
||||
from app.models import EnumE, Phase
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from mailbox import Message
|
||||
from smtplib import SMTP, SMTPServerDisconnected, SMTPRecipientsRefused
|
||||
from typing import Optional, Dict
|
||||
|
||||
import newrelic.agent
|
||||
from attr import dataclass
|
||||
|
||||
from app.config import (
|
||||
NOT_SEND_EMAIL,
|
||||
POSTFIX_SUBMISSION_TLS,
|
||||
POSTFIX_PORT,
|
||||
POSTFIX_SERVER,
|
||||
)
|
||||
from app.email import headers
|
||||
from app.log import LOG
|
||||
from app.message_utils import message_to_bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendRequest:
|
||||
envelope_from: str
|
||||
envelope_to: str
|
||||
msg: Message
|
||||
mail_options: Dict = {}
|
||||
rcpt_options: Dict = {}
|
||||
is_forward: bool = False
|
||||
ignore_smtp_errors: bool = False
|
||||
|
||||
|
||||
class MailSender:
|
||||
def __init__(self):
|
||||
self._pool: Optional[ThreadPoolExecutor] = None
|
||||
|
||||
def enable_background_pool(self, max_workers=10):
|
||||
self._pool = ThreadPoolExecutor(max_workers=max_workers)
|
||||
|
||||
def send(self, send_request: SendRequest, retries: int = 2):
|
||||
"""replace smtp.sendmail"""
|
||||
if NOT_SEND_EMAIL:
|
||||
LOG.d(
|
||||
"send email with subject '%s', from '%s' to '%s'",
|
||||
send_request.msg[headers.SUBJECT],
|
||||
send_request.msg[headers.FROM],
|
||||
send_request.msg[headers.TO],
|
||||
)
|
||||
return
|
||||
if not self._pool:
|
||||
self._send_to_smtp(send_request, retries)
|
||||
else:
|
||||
self._pool.submit(self._send_to_smtp, (send_request, retries))
|
||||
|
||||
def _send_to_smtp(self, send_request: SendRequest, retries: int):
|
||||
try:
|
||||
start = time.time()
|
||||
if POSTFIX_SUBMISSION_TLS:
|
||||
smtp_port = 587
|
||||
else:
|
||||
smtp_port = POSTFIX_PORT
|
||||
|
||||
with SMTP(POSTFIX_SERVER, smtp_port) as smtp:
|
||||
if POSTFIX_SUBMISSION_TLS:
|
||||
smtp.starttls()
|
||||
|
||||
elapsed = time.time() - start
|
||||
LOG.d("getting a smtp connection takes seconds %s", elapsed)
|
||||
newrelic.agent.record_custom_metric(
|
||||
"Custom/smtp_connection_time", elapsed
|
||||
)
|
||||
|
||||
# smtp.send_message has UnicodeEncodeError
|
||||
# encode message raw directly instead
|
||||
LOG.d(
|
||||
"Sendmail mail_from:%s, rcpt_to:%s, header_from:%s, header_to:%s, header_cc:%s",
|
||||
send_request.envelope_from,
|
||||
send_request.envelope_to,
|
||||
send_request.msg[headers.FROM],
|
||||
send_request.msg[headers.TO],
|
||||
send_request.msg[headers.CC],
|
||||
)
|
||||
smtp.sendmail(
|
||||
send_request.envelope_from,
|
||||
send_request.envelope_to,
|
||||
message_to_bytes(send_request.msg),
|
||||
send_request.mail_options,
|
||||
send_request.rcpt_options,
|
||||
)
|
||||
|
||||
newrelic.agent.record_custom_metric(
|
||||
"Custom/smtp_sending_time", time.time() - start
|
||||
)
|
||||
except (SMTPServerDisconnected, SMTPRecipientsRefused) as e:
|
||||
if retries > 0:
|
||||
LOG.w(
|
||||
"SMTPServerDisconnected or SMTPRecipientsRefused error %s, retry",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
time.sleep(0.3 * send_request.retries)
|
||||
self._send_to_smtp(send_request, retries - 1)
|
||||
else:
|
||||
if send_request.ignore_smtp_error:
|
||||
LOG.w("Ignore smtp error %s", e)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
mail_sender = MailSender()
|
||||
|
||||
|
||||
def sl_sendmail(
|
||||
envelope_from: str,
|
||||
envelope_to: str,
|
||||
msg: Message,
|
||||
mail_options=(),
|
||||
rcpt_options=(),
|
||||
is_forward: bool = False,
|
||||
retries=2,
|
||||
ignore_smtp_error=False,
|
||||
):
|
||||
send_request = SendRequest(
|
||||
envelope_from,
|
||||
envelope_to,
|
||||
msg,
|
||||
mail_options,
|
||||
rcpt_options,
|
||||
is_forward,
|
||||
ignore_smtp_error,
|
||||
)
|
||||
mail_sender.send(send_request, retries)
|
|
@ -0,0 +1,21 @@
|
|||
from email import policy
|
||||
from email.message import Message
|
||||
|
||||
from app.log import LOG
|
||||
|
||||
|
||||
def message_to_bytes(msg: Message) -> bytes:
|
||||
"""replace Message.as_bytes() method by trying different policies"""
|
||||
for generator_policy in [None, policy.SMTP, policy.SMTPUTF8]:
|
||||
try:
|
||||
return msg.as_bytes(policy=generator_policy)
|
||||
except:
|
||||
LOG.w("as_bytes() fails with %s policy", policy, exc_info=True)
|
||||
|
||||
msg_string = msg.as_string()
|
||||
try:
|
||||
return msg_string.encode()
|
||||
except:
|
||||
LOG.w("as_string().encode() fails", exc_info=True)
|
||||
|
||||
return msg_string.encode(errors="replace")
|
|
@ -690,6 +690,9 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
|||
Whether user can create a new alias. User can't create a new alias if
|
||||
- has more than 15 aliases in the free plan, *even in the free trial*
|
||||
"""
|
||||
if self.disabled:
|
||||
return False
|
||||
|
||||
if self.lifetime_or_active_subscription():
|
||||
return True
|
||||
else:
|
||||
|
|
|
@ -312,6 +312,9 @@ Input:
|
|||
- `page_id` in query. Used for the pagination. The endpoint returns maximum 20 aliases for each page. `page_id` starts
|
||||
at 0.
|
||||
- (Optional) `pinned` in query. If set, only pinned aliases are returned.
|
||||
- (Optional) `disabled` in query. If set, only disabled aliases are returned.
|
||||
- (Optional) `enabled` in query. If set, only enabled aliases are returned.
|
||||
Please note `pinned`, `disabled`, `enabled` are exclusive, i.e. only one can be present.
|
||||
- (Optional) query: included in request body. Some frameworks might prevent GET request having a non-empty body, in this
|
||||
case this endpoint also supports POST.
|
||||
|
||||
|
|
|
@ -100,7 +100,6 @@ from app.email_utils import (
|
|||
send_email_with_rate_control,
|
||||
get_email_domain_part,
|
||||
copy,
|
||||
to_bytes,
|
||||
send_email_at_most_times,
|
||||
is_valid_alias_address_domain,
|
||||
should_add_dkim_signature,
|
||||
|
@ -114,7 +113,6 @@ from app.email_utils import (
|
|||
should_disable,
|
||||
parse_id_from_bounce,
|
||||
spf_pass,
|
||||
sl_sendmail,
|
||||
sanitize_header,
|
||||
get_queue_id,
|
||||
should_ignore_bounce,
|
||||
|
@ -145,6 +143,8 @@ from app.handler.provider_complaint import (
|
|||
handle_yahoo_complaint,
|
||||
)
|
||||
from app.log import LOG, set_message_id
|
||||
from app.mail_sender import sl_sendmail
|
||||
from app.message_utils import message_to_bytes
|
||||
from app.models import (
|
||||
Alias,
|
||||
Contact,
|
||||
|
@ -497,7 +497,7 @@ def prepare_pgp_message(
|
|||
|
||||
# encrypt
|
||||
# use pgpy as fallback
|
||||
msg_bytes = to_bytes(clone_msg)
|
||||
msg_bytes = message_to_bytes(clone_msg)
|
||||
try:
|
||||
encrypted_data = pgp_utils.encrypt_file(BytesIO(msg_bytes), pgp_fingerprint)
|
||||
second.set_payload(encrypted_data)
|
||||
|
@ -523,11 +523,11 @@ def sign_msg(msg: Message) -> Message:
|
|||
signature.add_header("Content-Disposition", 'attachment; filename="signature.asc"')
|
||||
|
||||
try:
|
||||
signature.set_payload(sign_data(to_bytes(msg).replace(b"\n", b"\r\n")))
|
||||
signature.set_payload(sign_data(message_to_bytes(msg).replace(b"\n", b"\r\n")))
|
||||
except Exception:
|
||||
LOG.e("Cannot sign, try using pgpy")
|
||||
signature.set_payload(
|
||||
sign_data_with_pgpy(to_bytes(msg).replace(b"\n", b"\r\n"))
|
||||
sign_data_with_pgpy(message_to_bytes(msg).replace(b"\n", b"\r\n"))
|
||||
)
|
||||
|
||||
container.attach(signature)
|
||||
|
@ -539,7 +539,9 @@ def handle_email_sent_to_ourself(alias, from_addr: str, msg: Message, user):
|
|||
# store the refused email
|
||||
random_name = str(uuid.uuid4())
|
||||
full_report_path = f"refused-emails/cycle-{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(full_report_path, BytesIO(to_bytes(msg)), random_name)
|
||||
s3.upload_email_from_bytesio(
|
||||
full_report_path, BytesIO(message_to_bytes(msg)), random_name
|
||||
)
|
||||
refused_email = RefusedEmail.create(
|
||||
path=None, full_report_path=full_report_path, user_id=alias.user_id
|
||||
)
|
||||
|
@ -1390,7 +1392,7 @@ def handle_bounce_forward_phase(msg: Message, email_log: EmailLog):
|
|||
|
||||
full_report_path = f"refused-emails/full-{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(
|
||||
full_report_path, BytesIO(to_bytes(msg)), f"full-{random_name}"
|
||||
full_report_path, BytesIO(message_to_bytes(msg)), f"full-{random_name}"
|
||||
)
|
||||
|
||||
file_path = None
|
||||
|
@ -1409,7 +1411,7 @@ def handle_bounce_forward_phase(msg: Message, email_log: EmailLog):
|
|||
else:
|
||||
file_path = f"refused-emails/{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(
|
||||
file_path, BytesIO(to_bytes(orig_msg)), random_name
|
||||
file_path, BytesIO(message_to_bytes(orig_msg)), random_name
|
||||
)
|
||||
|
||||
refused_email = RefusedEmail.create(
|
||||
|
@ -1544,14 +1546,16 @@ def handle_bounce_reply_phase(envelope, msg: Message, email_log: EmailLog):
|
|||
random_name = str(uuid.uuid4())
|
||||
|
||||
full_report_path = f"refused-emails/full-{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(full_report_path, BytesIO(to_bytes(msg)), random_name)
|
||||
s3.upload_email_from_bytesio(
|
||||
full_report_path, BytesIO(message_to_bytes(msg)), random_name
|
||||
)
|
||||
|
||||
orig_msg = get_orig_message_from_bounce(msg)
|
||||
file_path = None
|
||||
if orig_msg:
|
||||
file_path = f"refused-emails/{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(
|
||||
file_path, BytesIO(to_bytes(orig_msg)), random_name
|
||||
file_path, BytesIO(message_to_bytes(orig_msg)), random_name
|
||||
)
|
||||
|
||||
refused_email = RefusedEmail.create(
|
||||
|
@ -1620,13 +1624,15 @@ def handle_spam(
|
|||
random_name = str(uuid.uuid4())
|
||||
|
||||
full_report_path = f"spams/full-{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(full_report_path, BytesIO(to_bytes(msg)), random_name)
|
||||
s3.upload_email_from_bytesio(
|
||||
full_report_path, BytesIO(message_to_bytes(msg)), random_name
|
||||
)
|
||||
|
||||
file_path = None
|
||||
if orig_msg:
|
||||
file_path = f"spams/{random_name}.eml"
|
||||
s3.upload_email_from_bytesio(
|
||||
file_path, BytesIO(to_bytes(orig_msg)), random_name
|
||||
file_path, BytesIO(message_to_bytes(orig_msg)), random_name
|
||||
)
|
||||
|
||||
refused_email = RefusedEmail.create(
|
||||
|
|
|
@ -317,7 +317,7 @@ cron = ["capturer (>=2.4)"]
|
|||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "6.3.1"
|
||||
version = "6.3.2"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
@ -673,24 +673,24 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
|||
|
||||
[[package]]
|
||||
name = "gevent"
|
||||
version = "20.9.0"
|
||||
version = "21.12.0"
|
||||
description = "Coroutine-based network library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
greenlet = {version = ">=0.4.17", markers = "platform_python_implementation == \"CPython\""}
|
||||
greenlet = {version = ">=1.1.0,<2.0", markers = "platform_python_implementation == \"CPython\""}
|
||||
"zope.event" = "*"
|
||||
"zope.interface" = "*"
|
||||
|
||||
[package.extras]
|
||||
dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"]
|
||||
docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput"]
|
||||
docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput", "zope.schema"]
|
||||
monitor = ["psutil (>=5.7.0)"]
|
||||
recommended = ["dnspython (>=1.16.0,<2.0)", "idna", "cffi (>=1.12.2)", "selectors2", "backports.socketpair", "psutil (>=5.7.0)"]
|
||||
test = ["dnspython (>=1.16.0,<2.0)", "idna", "requests", "objgraph", "cffi (>=1.12.2)", "selectors2", "futures", "mock", "backports.socketpair", "contextvars (==2.4)", "coverage (<5.0)", "coveralls (>=1.7.0)", "psutil (>=5.7.0)"]
|
||||
recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "backports.socketpair", "psutil (>=5.7.0)"]
|
||||
test = ["requests", "objgraph", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "futures", "mock", "backports.socketpair", "contextvars (==2.4)", "coverage (>=5.0)", "coveralls (>=1.7.0)", "psutil (>=5.7.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
|
@ -773,11 +773,14 @@ grpc = ["grpcio (>=1.0.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "0.4.17"
|
||||
version = "1.1.2"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx"]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
|
@ -1106,7 +1109,7 @@ ptyprocess = ">=0.5"
|
|||
|
||||
[[package]]
|
||||
name = "pgpy"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
description = "Pretty Good Privacy for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -1219,11 +1222,11 @@ test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
|
|||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.8.6"
|
||||
version = "2.9.3"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "ptyprocess"
|
||||
|
@ -1956,7 +1959,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "078a49e90e6d61fbb0673113c6300dd06ab5faa9a6fcc7264914fa32e21b8f5c"
|
||||
content-hash = "06d37c9f592a76f563a1424d04d9bd842131dda575ef5b261bfa474e3079c23f"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
|
@ -2143,47 +2146,47 @@ coloredlogs = [
|
|||
{file = "coloredlogs-14.0.tar.gz", hash = "sha256:a1fab193d2053aa6c0a97608c4342d031f1f93a3d1218432c59322441d31a505"},
|
||||
]
|
||||
coverage = [
|
||||
{file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"},
|
||||
{file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"},
|
||||
{file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"},
|
||||
{file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"},
|
||||
{file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"},
|
||||
{file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"},
|
||||
{file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"},
|
||||
{file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"},
|
||||
{file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"},
|
||||
{file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"},
|
||||
{file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"},
|
||||
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
|
||||
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
|
||||
]
|
||||
crontab = [
|
||||
{file = "crontab-0.22.8.tar.gz", hash = "sha256:1ac977fb1b8ba5b7b58e6f713cd7df36e61d7aee4c2b809abcf76adddd2deeaf"},
|
||||
|
@ -2298,30 +2301,39 @@ future = [
|
|||
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
|
||||
]
|
||||
gevent = [
|
||||
{file = "gevent-20.9.0-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:1628a403fc9c3ea9b35924638a4d4fbe236f60ecdf4e22ed133fbbaf0bc7cb6b"},
|
||||
{file = "gevent-20.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:283a021a2e14adfad718346f18982b80569d9c3a59e97cfae1b7d4c5b017941a"},
|
||||
{file = "gevent-20.9.0-cp27-cp27m-win32.whl", hash = "sha256:315a63a35068183dfb9bc0331c7bb3c265ee7db8a11797cbe98dadbdb45b5d35"},
|
||||
{file = "gevent-20.9.0-cp27-cp27m-win_amd64.whl", hash = "sha256:324808a8558c733f7a9734525483795d52ca3bbd5662b24b361d81c075414b1f"},
|
||||
{file = "gevent-20.9.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:2aa70726ad1883fe7c17774e5ccc91ac6e30334efa29bafb9b8fe8ca6091b219"},
|
||||
{file = "gevent-20.9.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:dd4c6b2f540b25c3d0f277a725bc1a900ce30a681b90a081216e31f814be453b"},
|
||||
{file = "gevent-20.9.0-cp35-cp35m-win32.whl", hash = "sha256:1cfa3674866294623e324fa5b76eba7b96744d1956a605cfe24d26c5cd890f91"},
|
||||
{file = "gevent-20.9.0-cp35-cp35m-win_amd64.whl", hash = "sha256:906175e3fb25f377a0b581e79d3ed5a7d925c136ff92fd022bb3013e25f5f3a9"},
|
||||
{file = "gevent-20.9.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:fb33dc1ab27557bccd64ad4bf81e68c8b0d780fe937b1e2c0814558798137229"},
|
||||
{file = "gevent-20.9.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:eba19bae532d0c48d489fa16815b242ce074b1f4b63e8a8e663232cbe311ead9"},
|
||||
{file = "gevent-20.9.0-cp36-cp36m-win32.whl", hash = "sha256:db208e74a32cff7f55f5aa1ba5d7d1c1a086a6325c8702ae78a5c741155552ff"},
|
||||
{file = "gevent-20.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2269574444113cb4ca1c1808ab9460a87fe25e1c34a6e36d975d4af46e4afff9"},
|
||||
{file = "gevent-20.9.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:adbb267067f56696b2babced3d0856aa39dcf14b8ccd2dffa1fab587b00c6f80"},
|
||||
{file = "gevent-20.9.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:9bb477f514cf39dc20651b479bf1ad4f38b9a679be2bfa3e162ec0c3785dfa2a"},
|
||||
{file = "gevent-20.9.0-cp37-cp37m-win32.whl", hash = "sha256:10110d4881aec04f218c316cb796b18c8b2cac67ae0eb5b0c5780056757268a2"},
|
||||
{file = "gevent-20.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e11de4b4d107ca2f35000eb08e9c4c4621c153103b400f48a9ea95b96d8c7e0b"},
|
||||
{file = "gevent-20.9.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:a8733a01974433d91308f8c44fa6cc13428b15bb39d46540657e260ff8852cb1"},
|
||||
{file = "gevent-20.9.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:afc177c37de41ce9c27d351ac84cbaf34407effcab5d6641645838f39d365be1"},
|
||||
{file = "gevent-20.9.0-cp38-cp38-win32.whl", hash = "sha256:93980e51dd2e5f81899d644a0b6ef4a73008c679fcedd50e3b21cc3451ba2424"},
|
||||
{file = "gevent-20.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:b2948566003a1030e47507755fe1f446995e8671c0c67571091539e01faf94cc"},
|
||||
{file = "gevent-20.9.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b07fcbca3e819296979d82fac3d8b44f0d5ced57b9a04dffcfd194da99c8eb2d"},
|
||||
{file = "gevent-20.9.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33a63f230755c6813fca39d9cea2a8894df32df2ee58fd69d8bf8fcc1d8e018e"},
|
||||
{file = "gevent-20.9.0-pp27-pypy_73-win32.whl", hash = "sha256:8d338cd6d040fe2607e5305dd7991b5960b3780ae01f804c2ac5760d31d3b2c6"},
|
||||
{file = "gevent-20.9.0.tar.gz", hash = "sha256:5f6d48051d336561ec08995431ee4d265ac723a64bba99cc58c3eb1a4d4f5c8d"},
|
||||
{file = "gevent-21.12.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481"},
|
||||
{file = "gevent-21.12.0-cp27-cp27m-win32.whl", hash = "sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101"},
|
||||
{file = "gevent-21.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f"},
|
||||
{file = "gevent-21.12.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d"},
|
||||
{file = "gevent-21.12.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a"},
|
||||
{file = "gevent-21.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f"},
|
||||
{file = "gevent-21.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a"},
|
||||
{file = "gevent-21.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397"},
|
||||
{file = "gevent-21.12.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3"},
|
||||
{file = "gevent-21.12.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9"},
|
||||
{file = "gevent-21.12.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50"},
|
||||
{file = "gevent-21.12.0-cp36-cp36m-win32.whl", hash = "sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02"},
|
||||
{file = "gevent-21.12.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24"},
|
||||
{file = "gevent-21.12.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54"},
|
||||
{file = "gevent-21.12.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c"},
|
||||
{file = "gevent-21.12.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6"},
|
||||
{file = "gevent-21.12.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4"},
|
||||
{file = "gevent-21.12.0-cp37-cp37m-win32.whl", hash = "sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214"},
|
||||
{file = "gevent-21.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452"},
|
||||
{file = "gevent-21.12.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea"},
|
||||
{file = "gevent-21.12.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692"},
|
||||
{file = "gevent-21.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd"},
|
||||
{file = "gevent-21.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766"},
|
||||
{file = "gevent-21.12.0-cp38-cp38-win32.whl", hash = "sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1"},
|
||||
{file = "gevent-21.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b"},
|
||||
{file = "gevent-21.12.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749"},
|
||||
{file = "gevent-21.12.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb"},
|
||||
{file = "gevent-21.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881"},
|
||||
{file = "gevent-21.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1"},
|
||||
{file = "gevent-21.12.0-cp39-cp39-win32.whl", hash = "sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682"},
|
||||
{file = "gevent-21.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279"},
|
||||
{file = "gevent-21.12.0-pp27-pypy_73-win_amd64.whl", hash = "sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f"},
|
||||
{file = "gevent-21.12.0.tar.gz", hash = "sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e"},
|
||||
]
|
||||
google-api-core = [
|
||||
{file = "google-api-core-1.22.2.tar.gz", hash = "sha256:779107f17e0fef8169c5239d56a8fbff03f9f72a3893c0c9e5842ec29dfedd54"},
|
||||
|
@ -2344,24 +2356,61 @@ googleapis-common-protos = [
|
|||
{file = "googleapis_common_protos-1.52.0-py2.py3-none-any.whl", hash = "sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24"},
|
||||
]
|
||||
greenlet = [
|
||||
{file = "greenlet-0.4.17-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:75e4c27188f28149b74e7685809f9227410fd15432a4438fc48627f518577fa5"},
|
||||
{file = "greenlet-0.4.17-cp27-cp27m-win32.whl", hash = "sha256:3af587e9813f9bd8be9212722321a5e7be23b2bc37e6323a90e592ab0c2ef117"},
|
||||
{file = "greenlet-0.4.17-cp27-cp27m-win_amd64.whl", hash = "sha256:ccd62f09f90b2730150d82f2f2ffc34d73c6ce7eac234aed04d15dc8a3023994"},
|
||||
{file = "greenlet-0.4.17-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:13037e2d7ab2145300676852fa069235512fdeba4ed1e3bb4b0677a04223c525"},
|
||||
{file = "greenlet-0.4.17-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:e495096e3e2e8f7192afb6aaeba19babc4fb2bdf543d7b7fed59e00c1df7f170"},
|
||||
{file = "greenlet-0.4.17-cp35-cp35m-win32.whl", hash = "sha256:124a3ae41215f71dc91d1a3d45cbf2f84e46b543e5d60b99ecc20e24b4c8f272"},
|
||||
{file = "greenlet-0.4.17-cp35-cp35m-win_amd64.whl", hash = "sha256:5494e3baeacc371d988345fbf8aa4bd15555b3077c40afcf1994776bb6d77eaf"},
|
||||
{file = "greenlet-0.4.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bee111161420f341a346731279dd976be161b465c1286f82cc0779baf7b729e8"},
|
||||
{file = "greenlet-0.4.17-cp36-cp36m-win32.whl", hash = "sha256:ac85db59aa43d78547f95fc7b6fd2913e02b9e9b09e2490dfb7bbdf47b2a4914"},
|
||||
{file = "greenlet-0.4.17-cp36-cp36m-win_amd64.whl", hash = "sha256:4481002118b2f1588fa3d821936ffdc03db80ef21186b62b90c18db4ba5e743b"},
|
||||
{file = "greenlet-0.4.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:be7a79988b8fdc5bbbeaed69e79cfb373da9759242f1565668be4fb7f3f37552"},
|
||||
{file = "greenlet-0.4.17-cp37-cp37m-win32.whl", hash = "sha256:97f2b01ab622a4aa4b3724a3e1fba66f47f054c434fbaa551833fa2b41e3db51"},
|
||||
{file = "greenlet-0.4.17-cp37-cp37m-win_amd64.whl", hash = "sha256:d3436110ca66fe3981031cc6aff8cc7a40d8411d173dde73ddaa5b8445385e2d"},
|
||||
{file = "greenlet-0.4.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a34023b9eabb3525ee059f3bf33a417d2e437f7f17e341d334987d4091ae6072"},
|
||||
{file = "greenlet-0.4.17-cp38-cp38-win32.whl", hash = "sha256:e66a824f44892bc4ec66c58601a413419cafa9cec895e63d8da889c8a1a4fa4a"},
|
||||
{file = "greenlet-0.4.17-cp38-cp38-win_amd64.whl", hash = "sha256:47825c3a109f0331b1e54c1173d4e57fa000aa6c96756b62852bfa1af91cd652"},
|
||||
{file = "greenlet-0.4.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1023d7b43ca11264ab7052cb09f5635d4afdb43df55e0854498fc63070a0b206"},
|
||||
{file = "greenlet-0.4.17.tar.gz", hash = "sha256:41d8835c69a78de718e466dd0e6bfd4b46125f21a67c3ff6d76d8d8059868d6b"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"},
|
||||
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"},
|
||||
{file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"},
|
||||
{file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"},
|
||||
{file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"},
|
||||
{file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"},
|
||||
{file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"},
|
||||
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
|
||||
{file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
|
||||
]
|
||||
gunicorn = [
|
||||
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
|
||||
|
@ -2550,8 +2599,8 @@ pexpect = [
|
|||
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
|
||||
]
|
||||
pgpy = [
|
||||
{file = "PGPy-0.5.3-py2.py3-none-any.whl", hash = "sha256:cba6fbbb44a896a8a4f5807b3d8d4943a8f7a6607be11587f4a27734c711c1dd"},
|
||||
{file = "PGPy-0.5.3.tar.gz", hash = "sha256:a49c269cedcaf82ac6999bcae5fd3f543ecb1c759f9d48a15ad8d8fa4ac03987"},
|
||||
{file = "PGPy-0.5.4-py2.py3-none-any.whl", hash = "sha256:c29ad9b2bcba6575c3773410894e77a7552b6a3de184fd99b4da3995986f26a9"},
|
||||
{file = "PGPy-0.5.4.tar.gz", hash = "sha256:bdd3da1e006fc8e81cc02232969924d6e8c98a4af1621a925d99bba09164183b"},
|
||||
]
|
||||
phpserialize = [
|
||||
{file = "phpserialize-1.3.tar.gz", hash = "sha256:bf672d312d203d09a84c26366fab8f438a3ffb355c407e69974b7ef2d39a0fa7"},
|
||||
|
@ -2616,41 +2665,62 @@ psutil = [
|
|||
{file = "psutil-5.7.2.tar.gz", hash = "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb"},
|
||||
]
|
||||
psycopg2-binary = [
|
||||
{file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"},
|
||||
{file = "psycopg2_binary-2.8.6-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4"},
|
||||
{file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db"},
|
||||
{file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"},
|
||||
{file = "psycopg2_binary-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25"},
|
||||
{file = "psycopg2_binary-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c"},
|
||||
{file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c"},
|
||||
{file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1"},
|
||||
{file = "psycopg2_binary-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2"},
|
||||
{file = "psycopg2_binary-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152"},
|
||||
{file = "psycopg2_binary-2.8.6-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"},
|
||||
{file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859"},
|
||||
{file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550"},
|
||||
{file = "psycopg2_binary-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd"},
|
||||
{file = "psycopg2_binary-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71"},
|
||||
{file = "psycopg2_binary-2.8.6-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4"},
|
||||
{file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb"},
|
||||
{file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da"},
|
||||
{file = "psycopg2_binary-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2"},
|
||||
{file = "psycopg2_binary-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a"},
|
||||
{file = "psycopg2_binary-2.8.6-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"},
|
||||
{file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf"},
|
||||
{file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b"},
|
||||
{file = "psycopg2_binary-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67"},
|
||||
{file = "psycopg2_binary-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66"},
|
||||
{file = "psycopg2_binary-2.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f"},
|
||||
{file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77"},
|
||||
{file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94"},
|
||||
{file = "psycopg2_binary-2.8.6-cp38-cp38-win32.whl", hash = "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729"},
|
||||
{file = "psycopg2_binary-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77"},
|
||||
{file = "psycopg2_binary-2.8.6-cp39-cp39-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83"},
|
||||
{file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52"},
|
||||
{file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd"},
|
||||
{file = "psycopg2_binary-2.8.6-cp39-cp39-win32.whl", hash = "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056"},
|
||||
{file = "psycopg2_binary-2.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6"},
|
||||
{file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"},
|
||||
{file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"},
|
||||
{file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"},
|
||||
{file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"},
|
||||
{file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"},
|
||||
{file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"},
|
||||
]
|
||||
ptyprocess = [
|
||||
{file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"},
|
||||
|
|
|
@ -26,6 +26,9 @@ authors = ["SimpleLogin <dev@simplelogin.io>"]
|
|||
license = "MIT"
|
||||
repository = "https://github.com/simple-login/app"
|
||||
keywords = ["email", "alias", "privacy", "oauth2", "openid"]
|
||||
packages = [
|
||||
{ include = "app/" }
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
|
@ -39,7 +42,7 @@ bcrypt = "^3.2.0"
|
|||
python-dotenv = "^0.14.0"
|
||||
ipython = "^7.31.1"
|
||||
sqlalchemy_utils = "^0.36.8"
|
||||
psycopg2-binary = "^2.8.6"
|
||||
psycopg2-binary = "^2.9.3"
|
||||
sentry_sdk = "^1.4.3"
|
||||
blinker = "^1.4"
|
||||
arrow = "^0.16.0"
|
||||
|
@ -71,10 +74,10 @@ webauthn = "^0.4.7"
|
|||
pyspf = "^2.0.14"
|
||||
Flask-Limiter = "^1.4"
|
||||
memory_profiler = "^0.57.0"
|
||||
gevent = "^20.9.0"
|
||||
gevent = "^21.12.0"
|
||||
aiospamc = "^0.6.1"
|
||||
email_validator = "^1.1.1"
|
||||
PGPy = "^0.5.3"
|
||||
PGPy = "0.5.4"
|
||||
coinbase-commerce = "^1.0.1"
|
||||
requests = "^2.25.1"
|
||||
newrelic = "^6.4.4"
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="192" height="192" viewBox="0 0 192 192">
|
||||
<defs>
|
||||
<linearGradient id="linear-gradient" y1="0.5" x2="1" y2="0.5" gradientUnits="objectBoundingBox">
|
||||
<stop offset="0" stop-color="#e3156a"/>
|
||||
<stop offset="1" stop-color="#91187f"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip-logo_without_text">
|
||||
<rect width="192" height="192"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="logo_without_text" data-name="logo without text" clip-path="url(#clip-logo_without_text)">
|
||||
<g id="Group_46" data-name="Group 46" transform="translate(14 35)">
|
||||
<g id="Group_45" data-name="Group 45" transform="translate(0 0)">
|
||||
<g id="Group_42" data-name="Group 42" transform="translate(13.064 34.188)">
|
||||
<path id="Path_170" data-name="Path 170" d="M-1461.505,851.181a70.211,70.211,0,0,1-30.587-17.439,70.253,70.253,0,0,1-30.74,17.481,49.776,49.776,0,0,0-.882,9.351,49.745,49.745,0,0,0,30.936,45.848,49.54,49.54,0,0,0,32.08-46.3A49.912,49.912,0,0,0-1461.505,851.181Zm-32.907,36.595h-.009l-18.858-18.857,5.8-5.813,13.07,13.07,20.44-20.446,5.805,5.8Zm32.907-36.595a70.211,70.211,0,0,1-30.587-17.439,70.253,70.253,0,0,1-30.74,17.481,49.776,49.776,0,0,0-.882,9.351,49.745,49.745,0,0,0,30.936,45.848,49.54,49.54,0,0,0,32.08-46.3A49.912,49.912,0,0,0-1461.505,851.181Zm-32.907,36.595h-.009l-18.858-18.857,5.8-5.813,13.07,13.07,20.44-20.446,5.805,5.8Zm2.319-54.034a70.253,70.253,0,0,1-30.74,17.481,49.776,49.776,0,0,0-.882,9.351,49.745,49.745,0,0,0,30.936,45.848,49.54,49.54,0,0,0,32.08-46.3,49.912,49.912,0,0,0-.807-8.94A70.211,70.211,0,0,1-1492.092,833.742Zm-2.319,54.034h-.009l-18.858-18.857,5.8-5.813,13.07,13.07,20.44-20.446,5.805,5.8Zm32.907-36.595a70.211,70.211,0,0,1-30.587-17.439,70.253,70.253,0,0,1-30.74,17.481,49.776,49.776,0,0,0-.882,9.351,49.745,49.745,0,0,0,30.936,45.848,49.54,49.54,0,0,0,32.08-46.3A49.912,49.912,0,0,0-1461.505,851.181Zm-32.907,36.595h-.009l-18.858-18.857,5.8-5.813,13.07,13.07,20.44-20.446,5.805,5.8Zm32.907-36.595a70.211,70.211,0,0,1-30.587-17.439,70.253,70.253,0,0,1-30.74,17.481,49.776,49.776,0,0,0-.882,9.351,49.745,49.745,0,0,0,30.936,45.848,49.54,49.54,0,0,0,32.08-46.3A49.912,49.912,0,0,0-1461.505,851.181Zm-32.907,36.595h-.009l-18.858-18.857,5.8-5.813,13.07,13.07,20.44-20.446,5.805,5.8Z" transform="translate(1523.715 -833.742)" fill="url(#linear-gradient)"/>
|
||||
</g>
|
||||
<g id="Group_44" data-name="Group 44">
|
||||
<g id="Group_43" data-name="Group 43">
|
||||
<path id="Path_171" data-name="Path 171" d="M-1365.831,827.564a13.031,13.031,0,0,0-1.9-2.387c-.221-.224-.458-.436-.7-.64a12.572,12.572,0,0,0-7.488-3.04h-95.881a12.571,12.571,0,0,0-7.488,3.04,6.589,6.589,0,0,0-.687.64,12.315,12.315,0,0,0-1.9,2.37,12.682,12.682,0,0,0-1.879,6.662v5l-1.589,1.639-.274.29a82.04,82.04,0,0,1-38.151,22.35c-.366.1-.737.2-1.111.29l-1.617.4-.357,1.622c-.125.586-.246,1.181-.363,1.778a67.17,67.17,0,0,0-1.192,12.612,66.477,66.477,0,0,0,12.037,38.276,66.735,66.735,0,0,0,27.909,22.956c.96.424,1.929.824,2.915,1.206l.927.357.932-.34c1.268-.449,2.516-.943,3.75-1.46a66.933,66.933,0,0,0,31.877-28.445h71.364a12.628,12.628,0,0,0,8.225-3.035c.257-.212.486-.425.723-.662a13.245,13.245,0,0,0,1.9-2.382,12.65,12.65,0,0,0,1.879-6.654v-65.8A12.741,12.741,0,0,0-1365.831,827.564Zm-105.859.009c.22-.017.443-.025.673-.025h94.334c.229,0,.45.008.67.025h1l-.161.145-46.692,41.311a2.983,2.983,0,0,1-3.946,0l-46.691-41.311-.162-.145Zm-5.975,6.636a6.509,6.509,0,0,1,.494-2.516l40.03,35.431-2.857,2.524q-.218-1.478-.511-2.932c-.05-.315-.109-.619-.178-.935l-.346-1.639-1.626-.4c-.391-.1-.781-.2-1.173-.3a82.218,82.218,0,0,1-33.833-18.366Zm-6.874,103.054a61.95,61.95,0,0,1-38.5-57.075,61.877,61.877,0,0,1,1.105-11.634,87.3,87.3,0,0,0,38.251-21.767,87.316,87.316,0,0,0,38.066,21.713,61.8,61.8,0,0,1,1.01,11.135A61.673,61.673,0,0,1-1484.539,937.263Zm39.56-30.6a66.556,66.556,0,0,0,5.73-27.024c0-.835-.017-1.658-.051-2.49l6.737-5.967,6.748,5.967a2.983,2.983,0,0,0,3.946,0l6.746-5.967,39.92,35.311.187.171Zm74.941-6.654a6.525,6.525,0,0,1-.492,2.507l-40-35.389,40.012-35.406a6.568,6.568,0,0,1,.483,2.49Z" transform="translate(1528.394 -821.497)" fill="url(#linear-gradient)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -170,3 +170,20 @@ textarea.parsley-error {
|
|||
.domain_detail_content {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Only show the help button on desktop */
|
||||
@media only screen and (max-width: 500px) {
|
||||
#help-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 500px) {
|
||||
#help-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#help-menu-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,14 @@
|
|||
<div class="container">
|
||||
<div class="d-flex">
|
||||
<a class="header-brand" href="{{ url_for('dashboard.index') }}">
|
||||
<img src="/static/logo.svg" class="header-brand-img" style="max-width: 8rem" alt="logo">
|
||||
<picture>
|
||||
<source
|
||||
media="(max-width: 650px)"
|
||||
srcset="/static/logo-without-text.svg"
|
||||
>
|
||||
|
||||
<img src="/static/logo.svg" class="header-brand-img" style="max-width: 8rem" alt="logo">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<div class="d-flex order-lg-2 ml-auto">
|
||||
|
@ -70,7 +77,7 @@
|
|||
</div>
|
||||
|
||||
{% if ZENDESK_ENABLED %}
|
||||
<div class="dropdown nav-item d-flex align-items-center">
|
||||
<div id="help-btn" class="dropdown nav-item align-items-center">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
||||
<div class="dropdown-item">
|
||||
|
|
|
@ -93,5 +93,30 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if ZENDESK_ENABLED %}
|
||||
<li class="nav-item">
|
||||
<div id="help-menu-item" class="dropdown nav-item align-items-center mb-3">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
||||
<div class="dropdown-item">
|
||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
||||
Docs
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a href="https://github.com/simple-login/app/discussions" target="_blank">
|
||||
Forum
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-item">
|
||||
<a href="/dashboard/support">Support</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</ul>
|
|
@ -195,6 +195,44 @@ def test_get_pinned_aliases_v2(flask_client):
|
|||
assert r.json["aliases"][0]["id"] == a0.id
|
||||
|
||||
|
||||
def test_get_disabled_aliases_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
a0 = Alias.create_new(user, "prefix0")
|
||||
a0.enabled = False
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0")
|
||||
assert r.status_code == 200
|
||||
# the default alias (created when user is created) and a0 are returned
|
||||
assert len(r.json["aliases"]) == 2
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0&disabled=true")
|
||||
assert r.status_code == 200
|
||||
# only a0 is returned
|
||||
assert len(r.json["aliases"]) == 1
|
||||
assert r.json["aliases"][0]["id"] == a0.id
|
||||
|
||||
|
||||
def test_get_enabled_aliases_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
a0 = Alias.create_new(user, "prefix0")
|
||||
a0.enabled = False
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0")
|
||||
assert r.status_code == 200
|
||||
# the default alias (created when user is created) and a0 are returned
|
||||
assert len(r.json["aliases"]) == 2
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0&enabled=true")
|
||||
assert r.status_code == 200
|
||||
# only the first alias is returned
|
||||
assert len(r.json["aliases"]) == 1
|
||||
assert r.json["aliases"][0]["id"] != a0.id
|
||||
|
||||
|
||||
def test_delete_alias(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
|
@ -593,3 +631,22 @@ def test_toggle_contact(flask_client):
|
|||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"block_forward": True}
|
||||
|
||||
|
||||
def test_get_aliases_disabled_account(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/v2/aliases?page_id=0",
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
user.disabled = True
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/v2/aliases?page_id=0",
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
|
|
|
@ -1,7 +1,23 @@
|
|||
from app.alias_utils import delete_alias, check_alias_prefix
|
||||
from typing import List
|
||||
|
||||
from app.alias_utils import (
|
||||
delete_alias,
|
||||
check_alias_prefix,
|
||||
get_user_if_alias_would_auto_create,
|
||||
try_auto_create,
|
||||
)
|
||||
from app.config import ALIAS_DOMAINS
|
||||
from app.db import Session
|
||||
from app.models import Alias, DeletedAlias
|
||||
from tests.utils import create_new_user
|
||||
from app.models import (
|
||||
Alias,
|
||||
DeletedAlias,
|
||||
CustomDomain,
|
||||
AutoCreateRule,
|
||||
Directory,
|
||||
DirectoryMailbox,
|
||||
User,
|
||||
)
|
||||
from tests.utils import create_new_user, random_domain, random_token
|
||||
|
||||
|
||||
def test_delete_alias(flask_client):
|
||||
|
@ -44,3 +60,75 @@ def test_check_alias_prefix(flask_client):
|
|||
assert not check_alias_prefix("a b")
|
||||
assert not check_alias_prefix("+👌")
|
||||
assert not check_alias_prefix("too-long" * 10)
|
||||
|
||||
|
||||
def get_auto_create_alias_tests(user: User) -> List:
|
||||
user.lifetime = True
|
||||
catchall = CustomDomain.create(
|
||||
user_id=user.id,
|
||||
catch_all=True,
|
||||
domain=random_domain(),
|
||||
verified=True,
|
||||
flush=True,
|
||||
)
|
||||
no_catchall = CustomDomain.create(
|
||||
user_id=user.id,
|
||||
catch_all=False,
|
||||
domain=random_domain(),
|
||||
verified=True,
|
||||
flush=True,
|
||||
)
|
||||
no_catchall_with_rule = CustomDomain.create(
|
||||
user_id=user.id,
|
||||
catch_all=False,
|
||||
domain=random_domain(),
|
||||
verified=True,
|
||||
flush=True,
|
||||
)
|
||||
AutoCreateRule.create(
|
||||
custom_domain_id=no_catchall_with_rule.id,
|
||||
order=0,
|
||||
regex="ok-.*",
|
||||
flush=True,
|
||||
)
|
||||
dir_name = random_token()
|
||||
directory = Directory.create(name=dir_name, user_id=user.id, flush=True)
|
||||
DirectoryMailbox.create(
|
||||
directory_id=directory.id, mailbox_id=user.default_mailbox_id, flush=True
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
return [
|
||||
(f"nonexistant@{catchall.domain}", True),
|
||||
(f"nonexistant@{no_catchall.domain}", False),
|
||||
(f"nonexistant@{no_catchall_with_rule.domain}", False),
|
||||
(f"ok-nonexistant@{no_catchall_with_rule.domain}", True),
|
||||
(f"{dir_name}+something@nowhere.net", False),
|
||||
(f"{dir_name}#something@nowhere.net", False),
|
||||
(f"{dir_name}/something@nowhere.net", False),
|
||||
(f"{dir_name}+something@{ALIAS_DOMAINS[0]}", True),
|
||||
(f"{dir_name}#something@{ALIAS_DOMAINS[0]}", True),
|
||||
(f"{dir_name}/something@{ALIAS_DOMAINS[0]}", True),
|
||||
]
|
||||
|
||||
|
||||
def test_get_user_if_alias_would_auto_create(flask_client):
|
||||
user = create_new_user()
|
||||
for test_id, (address, expected_ok) in enumerate(get_auto_create_alias_tests(user)):
|
||||
result = get_user_if_alias_would_auto_create(address)
|
||||
if expected_ok:
|
||||
assert (
|
||||
isinstance(result, User) and result.id == user.id
|
||||
), f"Case {test_id} - Failed address {address}"
|
||||
else:
|
||||
assert not result, f"Case {test_id} - Failed address {address}"
|
||||
|
||||
|
||||
def test_auto_create_alias(flask_client):
|
||||
user = create_new_user()
|
||||
for test_id, (address, expected_ok) in enumerate(get_auto_create_alias_tests(user)):
|
||||
result = try_auto_create(address)
|
||||
if expected_ok:
|
||||
assert result, f"Case {test_id} - Failed address {address}"
|
||||
else:
|
||||
assert result is None, f"Case {test_id} - Failed address {address}"
|
||||
|
|
|
@ -19,7 +19,6 @@ from app.email_utils import (
|
|||
get_header_from_bounce,
|
||||
is_valid_email,
|
||||
add_header,
|
||||
to_bytes,
|
||||
generate_reply_email,
|
||||
normalize_reply_email,
|
||||
get_encoding,
|
||||
|
@ -161,23 +160,6 @@ def test_send_email_with_rate_control(flask_client):
|
|||
)
|
||||
|
||||
|
||||
def test_copy():
|
||||
email_str = """
|
||||
From: abcd@gmail.com
|
||||
To: hey@example.org
|
||||
Subject: subject
|
||||
|
||||
Body
|
||||
"""
|
||||
msg = email.message_from_string(email_str)
|
||||
msg2 = copy(msg)
|
||||
assert to_bytes(msg) == to_bytes(msg2)
|
||||
|
||||
msg = email.message_from_string("👌")
|
||||
msg2 = copy(msg)
|
||||
assert to_bytes(msg) == to_bytes(msg2)
|
||||
|
||||
|
||||
def test_get_spam_from_header():
|
||||
is_spam, _ = get_spam_from_header(
|
||||
"""No, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
|
||||
|
@ -476,19 +458,6 @@ Content-Type: text/html; charset=us-ascii
|
|||
assert "old" not in new_msg.as_string()
|
||||
|
||||
|
||||
def test_to_bytes():
|
||||
msg = email.message_from_string("☕️ emoji")
|
||||
assert to_bytes(msg)
|
||||
# \n is appended when message is converted to bytes
|
||||
assert to_bytes(msg).decode() == "\n☕️ emoji"
|
||||
|
||||
msg = email.message_from_string("ascii")
|
||||
assert to_bytes(msg) == b"\nascii"
|
||||
|
||||
msg = email.message_from_string("éèà€")
|
||||
assert to_bytes(msg).decode() == "\néèà€"
|
||||
|
||||
|
||||
def test_generate_reply_email(flask_client):
|
||||
user = create_new_user()
|
||||
reply_email = generate_reply_email("test@example.org", user)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import email
|
||||
from app.email_utils import (
|
||||
copy,
|
||||
)
|
||||
from app.message_utils import message_to_bytes
|
||||
|
||||
|
||||
def test_copy():
|
||||
email_str = """
|
||||
From: abcd@gmail.com
|
||||
To: hey@example.org
|
||||
Subject: subject
|
||||
|
||||
Body
|
||||
"""
|
||||
msg = email.message_from_string(email_str)
|
||||
msg2 = copy(msg)
|
||||
assert message_to_bytes(msg) == message_to_bytes(msg2)
|
||||
|
||||
msg = email.message_from_string("👌")
|
||||
msg2 = copy(msg)
|
||||
assert message_to_bytes(msg) == message_to_bytes(msg2)
|
||||
|
||||
|
||||
def test_to_bytes():
|
||||
msg = email.message_from_string("☕️ emoji")
|
||||
assert message_to_bytes(msg)
|
||||
# \n is appended when message is converted to bytes
|
||||
assert message_to_bytes(msg).decode() == "\n☕️ emoji"
|
||||
|
||||
msg = email.message_from_string("ascii")
|
||||
assert message_to_bytes(msg) == b"\nascii"
|
||||
|
||||
msg = email.message_from_string("éèà€")
|
||||
assert message_to_bytes(msg).decode() == "\néèà€"
|
|
@ -250,3 +250,11 @@ def test_EnumE():
|
|||
|
||||
assert E.get_value("A") == 100
|
||||
assert E.get_value("Not existent") is None
|
||||
|
||||
|
||||
def test_can_create_new_alias_disabled_user():
|
||||
user = create_new_user()
|
||||
assert user.can_create_new_alias()
|
||||
|
||||
user.disabled = True
|
||||
assert not user.can_create_new_alias()
|
||||
|
|
Loading…
Reference in New Issue