diff --git a/.flake8 b/.flake8 index 8938b273..c57a1d75 100644 --- a/.flake8 +++ b/.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__, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..a0ee70e7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +## code changes will send PR to following users +* @acasajus @cquintana92 @nguyenkims \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 03d76a09..54186f8a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/app/alias_utils.py b/app/alias_utils.py index 8d55fdbc..af7bdc78 100644 --- a/app/alias_utils.py +++ b/app/alias_utils.py @@ -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: diff --git a/app/api/base.py b/app/api/base.py index dc1c86af..70151d8e 100644 --- a/app/api/base.py +++ b/app/api/base.py @@ -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 diff --git a/app/api/views/alias.py b/app/api/views/alias.py index cff85601..8c1a4575 100644 --- a/app/api/views/alias.py +++ b/app/api/views/alias.py @@ -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 ( diff --git a/app/auth/views/fido.py b/app/auth/views/fido.py index eba5ea5c..c65a6ff5 100644 --- a/app/auth/views/fido.py +++ b/app/auth/views/fido.py @@ -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", diff --git a/app/email/spam.py b/app/email/spam.py index c8a481be..656fcffe 100644 --- a/app/email/spam.py +++ b/app/email/spam.py @@ -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"): diff --git a/app/email_utils.py b/app/email_utils.py index 0561e399..76d7b920 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -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 + ) diff --git a/app/events/auth_event.py b/app/events/auth_event.py index 492c4e89..f6752186 100644 --- a/app/events/auth_event.py +++ b/app/events/auth_event.py @@ -1,4 +1,4 @@ -import newrelic +import newrelic.agent from app.models import EnumE diff --git a/app/handler/dmarc.py b/app/handler/dmarc.py index 62eed243..b5fb52fa 100644 --- a/app/handler/dmarc.py +++ b/app/handler/dmarc.py @@ -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 diff --git a/app/handler/spamd_result.py b/app/handler/spamd_result.py index e3bd3cb6..771eabc3 100644 --- a/app/handler/spamd_result.py +++ b/app/handler/spamd_result.py @@ -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 diff --git a/app/mail_sender.py b/app/mail_sender.py new file mode 100644 index 00000000..41c88a14 --- /dev/null +++ b/app/mail_sender.py @@ -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) diff --git a/app/message_utils.py b/app/message_utils.py new file mode 100644 index 00000000..a5a199bb --- /dev/null +++ b/app/message_utils.py @@ -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") diff --git a/app/models.py b/app/models.py index 57199479..a9d823b6 100644 --- a/app/models.py +++ b/app/models.py @@ -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: diff --git a/docs/api.md b/docs/api.md index a5332a83..04e2373c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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. diff --git a/email_handler.py b/email_handler.py index e90bef9a..3e211321 100644 --- a/email_handler.py +++ b/email_handler.py @@ -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( diff --git a/poetry.lock b/poetry.lock index 4c1c9ab2..cc441297 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index 8d59c897..9a2176c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,9 @@ authors = ["SimpleLogin "] 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" diff --git a/static/logo-without-text.svg b/static/logo-without-text.svg new file mode 100755 index 00000000..32131895 --- /dev/null +++ b/static/logo-without-text.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/style.css b/static/style.css index 0996d792..b125cd29 100644 --- a/static/style.css +++ b/static/style.css @@ -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; + } +} \ No newline at end of file diff --git a/templates/header.html b/templates/header.html index 656317ba..8d5b94af 100644 --- a/templates/header.html +++ b/templates/header.html @@ -2,7 +2,14 @@
- logo + + + + logo +
@@ -70,7 +77,7 @@
{% if ZENDESK_ENABLED %} -