Compare commits
2 Commits
26cc09f8e9
...
1d2ee61f5f
| Author | SHA256 | Date | |
|---|---|---|---|
| 1d2ee61f5f | |||
| fe497b5132 |
274
email_task.ipynb
274
email_task.ipynb
@ -38,7 +38,8 @@
|
||||
" load_env('env')\n",
|
||||
"except:\n",
|
||||
" load_env('env_example')"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -48,17 +49,33 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import smtplib\n",
|
||||
"import os\n",
|
||||
"from email.mime.application import MIMEApplication\n",
|
||||
"from email.mime.multipart import MIMEMultipart\n",
|
||||
"from email.mime.text import MIMEText\n",
|
||||
"from email.utils import formatdate, make_msgid\n",
|
||||
"from typing import Optional\n",
|
||||
"import gnupg\n",
|
||||
"from celery import Celery\n",
|
||||
"from config import *\n",
|
||||
"from db import *\n",
|
||||
"from templates import templates\n",
|
||||
"import logging\n",
|
||||
"from typing import Optional\n",
|
||||
"import asyncio \n",
|
||||
"from email.utils import formatdate, make_msgid\n"
|
||||
]
|
||||
"import asyncio \n"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ea1066a5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:\n",
|
||||
" return f\"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}\"\n"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -67,37 +84,211 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:\n",
|
||||
" return f\"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}\"\n",
|
||||
"GPG_HOME = os.getenv(\"GPG_HOME\", \"/var/lib/app/.gnupg\")\n",
|
||||
"GPG_BINARY = os.getenv(\"GPG_BINARY\", \"gpg\")\n",
|
||||
"\n",
|
||||
"gpg = gnupg.GPG(\n",
|
||||
" gnupghome=GPG_HOME,\n",
|
||||
" gpgbinary=GPG_BINARY,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def build_unsubscribe_url(\n",
|
||||
" to_email: str,\n",
|
||||
" user_id: Optional[int],\n",
|
||||
" domain: str,\n",
|
||||
") -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" Replace this implementation with your real unsubscribe URL builder.\n",
|
||||
" Avoid putting a raw email address in the URL in production.\n",
|
||||
" \"\"\"\n",
|
||||
" from urllib.parse import quote\n",
|
||||
"\n",
|
||||
" return f\"https://{domain}/unsubscribe?email={quote(to_email)}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def import_public_key(public_key: str) -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" Import an ASCII-armored public key and return its fingerprint.\n",
|
||||
"\n",
|
||||
" The database value should contain the complete public key, for example:\n",
|
||||
"\n",
|
||||
" -----BEGIN PGP PUBLIC KEY BLOCK-----\n",
|
||||
" ...\n",
|
||||
" -----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
" \"\"\"\n",
|
||||
" result = gpg.import_keys(public_key)\n",
|
||||
"\n",
|
||||
" if not result.fingerprints:\n",
|
||||
" raise RuntimeError(\n",
|
||||
" f\"Unable to import the recipient's public key: {result}\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Use the imported fingerprint rather than the email address. The email\n",
|
||||
" # address in the key's UID may not exactly match the recipient address.\n",
|
||||
" return result.fingerprints[0]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def encrypt_mime_message(\n",
|
||||
" message,\n",
|
||||
" recipient_fingerprint: str,\n",
|
||||
"):\n",
|
||||
" \"\"\"\n",
|
||||
" Encrypt an already-built MIME message and return a PGP/MIME message.\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" # Serialize the original MIME message before encryption.\n",
|
||||
" plaintext = message.as_bytes()\n",
|
||||
"\n",
|
||||
" # Binary OpenPGP data is appropriate for application/octet-stream.\n",
|
||||
" encrypted = gpg.encrypt(\n",
|
||||
" plaintext,\n",
|
||||
" recipients=[recipient_fingerprint],\n",
|
||||
" armor=False,\n",
|
||||
" always_trust=False,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" if not encrypted.ok:\n",
|
||||
" raise RuntimeError(\n",
|
||||
" f\"GPG encryption failed: {encrypted.status}; \"\n",
|
||||
" f\"stderr: {encrypted.stderr}\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" encrypted_bytes = encrypted.data\n",
|
||||
"\n",
|
||||
" if not encrypted_bytes:\n",
|
||||
" raise RuntimeError(\"GPG returned an empty encrypted message\")\n",
|
||||
"\n",
|
||||
" encrypted_message = MIMEMultipart(\n",
|
||||
" \"encrypted\",\n",
|
||||
" protocol=\"application/pgp-encrypted\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Copy normal mail headers. Content-Type and MIME-Version are generated\n",
|
||||
" # automatically for the new multipart/encrypted message.\n",
|
||||
" for header in (\n",
|
||||
" \"From\",\n",
|
||||
" \"To\",\n",
|
||||
" \"Subject\",\n",
|
||||
" \"Message-ID\",\n",
|
||||
" \"Date\",\n",
|
||||
" \"X-Priority\",\n",
|
||||
" \"Importance\",\n",
|
||||
" \"X-Auto-Response-Suppress\",\n",
|
||||
" \"List-Unsubscribe\",\n",
|
||||
" \"List-Unsubscribe-Post\",\n",
|
||||
" ):\n",
|
||||
" value = message.get(header)\n",
|
||||
" if value is not None:\n",
|
||||
" encrypted_message[header] = value\n",
|
||||
"\n",
|
||||
" # Required PGP/MIME version part.\n",
|
||||
" version_part = MIMEApplication(\n",
|
||||
" b\"Version: 1\\r\\n\",\n",
|
||||
" _subtype=\"pgp-encrypted\",\n",
|
||||
" )\n",
|
||||
" version_part.add_header(\n",
|
||||
" \"Content-Description\",\n",
|
||||
" \"PGP/MIME version identification\",\n",
|
||||
" )\n",
|
||||
" encrypted_message.attach(version_part)\n",
|
||||
"\n",
|
||||
" # Encrypted MIME payload.\n",
|
||||
" encrypted_part = MIMEApplication(\n",
|
||||
" encrypted_bytes,\n",
|
||||
" _subtype=\"octet-stream\",\n",
|
||||
" )\n",
|
||||
" encrypted_part.add_header(\n",
|
||||
" \"Content-Description\",\n",
|
||||
" \"OpenPGP encrypted message\",\n",
|
||||
" )\n",
|
||||
" encrypted_part.add_header(\n",
|
||||
" \"Content-Disposition\",\n",
|
||||
" \"inline\",\n",
|
||||
" filename=\"encrypted.asc\",\n",
|
||||
" )\n",
|
||||
" encrypted_message.attach(encrypted_part)\n",
|
||||
"\n",
|
||||
" return encrypted_message\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def create_email_message(\n",
|
||||
" to_email: str,\n",
|
||||
" subject: str,\n",
|
||||
" html_content: str,\n",
|
||||
" user_id: int = None,\n",
|
||||
" user_id: Optional[int] = None,\n",
|
||||
"):\n",
|
||||
" \"\"\"\n",
|
||||
" Create an HTML email.\n",
|
||||
"\n",
|
||||
" If the recipient has a public GPG key in the database, the complete\n",
|
||||
" MIME message is encrypted using PGP/MIME.\n",
|
||||
"\n",
|
||||
" Expected database structure:\n",
|
||||
"\n",
|
||||
" {\n",
|
||||
" \"email\": \"recipient@example.com\",\n",
|
||||
" \"gnupg\": \"-----BEGIN PGP PUBLIC KEY BLOCK-----\\\\n...\"\n",
|
||||
" }\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" msg = MIMEMultipart(\"alternative\")\n",
|
||||
" \n",
|
||||
" msg['From'] = f\"{FROM_NAME} <{FROM_EMAIL}>\"\n",
|
||||
" msg['To'] = to_email\n",
|
||||
" msg['Subject'] = subject\n",
|
||||
" msg['Message-ID'] = make_msgid(domain=FROM_EMAIL.split('@')[1])\n",
|
||||
" msg['Date'] = formatdate(localtime=True)\n",
|
||||
" msg['X-Priority'] = \"3\"\n",
|
||||
" msg['Importance'] = \"normal\"\n",
|
||||
" msg['X-Auto-Response-Suppress'] = \"All\"\n",
|
||||
" \n",
|
||||
" # token = await create_unsubscribe_token(to_email, user_id)\n",
|
||||
" unsubscribe_url = build_unsubscribe_url(to_email, None, FULL_DOMAIN)\n",
|
||||
" if to_email != FROM_EMAIL:\n",
|
||||
" msg['List-Unsubscribe'] = f\"<{unsubscribe_url}>\"\n",
|
||||
" msg['List-Unsubscribe-Post'] = \"List-Unsubscribe=One-Click\"\n",
|
||||
" \n",
|
||||
" msg.attach(MIMEText(html_content, 'html'))\n",
|
||||
" \n",
|
||||
" return msg\n"
|
||||
]
|
||||
"\n",
|
||||
" msg[\"From\"] = f\"{FROM_NAME} <{FROM_EMAIL}>\"\n",
|
||||
" msg[\"To\"] = to_email\n",
|
||||
" msg[\"Subject\"] = subject\n",
|
||||
" msg[\"Message-ID\"] = make_msgid(\n",
|
||||
" domain=FROM_EMAIL.rsplit(\"@\", 1)[1],\n",
|
||||
" )\n",
|
||||
" msg[\"Date\"] = formatdate(localtime=True)\n",
|
||||
" msg[\"X-Priority\"] = \"3\"\n",
|
||||
" msg[\"Importance\"] = \"normal\"\n",
|
||||
" msg[\"X-Auto-Response-Suppress\"] = \"All\"\n",
|
||||
"\n",
|
||||
" # Do not send unsubscribe headers to yourself.\n",
|
||||
" if to_email.lower() != FROM_EMAIL.lower():\n",
|
||||
" unsubscribe_url = build_unsubscribe_url(\n",
|
||||
" to_email=to_email,\n",
|
||||
" user_id=user_id,\n",
|
||||
" domain=FULL_DOMAIN,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" msg[\"List-Unsubscribe\"] = f\"<{unsubscribe_url}>\"\n",
|
||||
" msg[\"List-Unsubscribe-Post\"] = \"List-Unsubscribe=One-Click\"\n",
|
||||
"\n",
|
||||
" msg.attach(\n",
|
||||
" MIMEText(\n",
|
||||
" html_content,\n",
|
||||
" \"html\",\n",
|
||||
" \"utf-8\",\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Fetch the recipient's public key.\n",
|
||||
" user = await users_collection.find_one(\n",
|
||||
" {\"email\": to_email},\n",
|
||||
" {\"gnupg\": 1},\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" public_key = user.get(\"gnupg\") if user else None\n",
|
||||
"\n",
|
||||
" if not public_key:\n",
|
||||
" # Return a normal unencrypted email when no key is available.\n",
|
||||
" return msg\n",
|
||||
"\n",
|
||||
" if not isinstance(public_key, str):\n",
|
||||
" raise TypeError(\"The gnupg database field must be a string\")\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
" fingerprint = import_public_key(public_key)\n",
|
||||
" return encrypt_mime_message(msg, fingerprint)\n",
|
||||
"\n",
|
||||
" except Exception:\n",
|
||||
" # Encryption is mandatory whenever a key exists.\n",
|
||||
" # Do not silently send the message unencrypted.\n",
|
||||
" raise\n"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -140,7 +331,8 @@
|
||||
" f\"Subject: {subject}, Error: {str(e)}\"\n",
|
||||
" )\n",
|
||||
" return False\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -150,7 +342,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"celery_app_email = Celery('emails', broker=f'{redis_uri}2', backend=f'{redis_uri}2', task_track_started=True)\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -189,7 +382,8 @@
|
||||
" f\"Ошибка при отправке email на {to_email} (попытка {self.request.retries + 1}): {exc}\"\n",
|
||||
" )\n",
|
||||
" raise self.retry(exc=exc)\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -199,6 +393,18 @@
|
||||
"## Run"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3aeae7e8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!mkdir -p /var/lib/app/.gnupg\n",
|
||||
"!chmod 700 /var/lib/app/.gnupg"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
@ -228,7 +434,8 @@
|
||||
"\n",
|
||||
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
|
||||
" f.write('\\n\\n'.join(app))\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -238,7 +445,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!celery -A email_task.celery_app_email worker --loglevel=info"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
224
email_task.py
224
email_task.py
@ -32,48 +32,228 @@ except:
|
||||
load_env('env_example')
|
||||
|
||||
import smtplib
|
||||
import os
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, make_msgid
|
||||
from typing import Optional
|
||||
import gnupg
|
||||
from celery import Celery
|
||||
from config import *
|
||||
from db import *
|
||||
from templates import templates
|
||||
import logging
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
from email.utils import formatdate, make_msgid
|
||||
|
||||
|
||||
def build_unsubscribe_url(email: str, token: str, base_url: str) -> str:
|
||||
return f"{base_url}/api/v1/email/unsubscribe?token={token}&email={email}"
|
||||
|
||||
|
||||
GPG_HOME = os.getenv("GPG_HOME", "/var/lib/app/.gnupg")
|
||||
GPG_BINARY = os.getenv("GPG_BINARY", "gpg")
|
||||
|
||||
gpg = gnupg.GPG(
|
||||
gnupghome=GPG_HOME,
|
||||
gpgbinary=GPG_BINARY,
|
||||
)
|
||||
|
||||
|
||||
def build_unsubscribe_url(
|
||||
to_email: str,
|
||||
user_id: Optional[int],
|
||||
domain: str,
|
||||
) -> str:
|
||||
"""
|
||||
Replace this implementation with your real unsubscribe URL builder.
|
||||
Avoid putting a raw email address in the URL in production.
|
||||
"""
|
||||
from urllib.parse import quote
|
||||
|
||||
return f"https://{domain}/unsubscribe?email={quote(to_email)}"
|
||||
|
||||
|
||||
def import_public_key(public_key: str) -> str:
|
||||
"""
|
||||
Import an ASCII-armored public key and return its fingerprint.
|
||||
|
||||
The database value should contain the complete public key, for example:
|
||||
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
...
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
"""
|
||||
result = gpg.import_keys(public_key)
|
||||
|
||||
if not result.fingerprints:
|
||||
raise RuntimeError(
|
||||
f"Unable to import the recipient's public key: {result}"
|
||||
)
|
||||
|
||||
# Use the imported fingerprint rather than the email address. The email
|
||||
# address in the key's UID may not exactly match the recipient address.
|
||||
return result.fingerprints[0]
|
||||
|
||||
|
||||
def encrypt_mime_message(
|
||||
message,
|
||||
recipient_fingerprint: str,
|
||||
):
|
||||
"""
|
||||
Encrypt an already-built MIME message and return a PGP/MIME message.
|
||||
"""
|
||||
|
||||
# Serialize the original MIME message before encryption.
|
||||
plaintext = message.as_bytes()
|
||||
|
||||
# Binary OpenPGP data is appropriate for application/octet-stream.
|
||||
encrypted = gpg.encrypt(
|
||||
plaintext,
|
||||
recipients=[recipient_fingerprint],
|
||||
armor=False,
|
||||
always_trust=False,
|
||||
)
|
||||
|
||||
if not encrypted.ok:
|
||||
raise RuntimeError(
|
||||
f"GPG encryption failed: {encrypted.status}; "
|
||||
f"stderr: {encrypted.stderr}"
|
||||
)
|
||||
|
||||
encrypted_bytes = encrypted.data
|
||||
|
||||
if not encrypted_bytes:
|
||||
raise RuntimeError("GPG returned an empty encrypted message")
|
||||
|
||||
encrypted_message = MIMEMultipart(
|
||||
"encrypted",
|
||||
protocol="application/pgp-encrypted",
|
||||
)
|
||||
|
||||
# Copy normal mail headers. Content-Type and MIME-Version are generated
|
||||
# automatically for the new multipart/encrypted message.
|
||||
for header in (
|
||||
"From",
|
||||
"To",
|
||||
"Subject",
|
||||
"Message-ID",
|
||||
"Date",
|
||||
"X-Priority",
|
||||
"Importance",
|
||||
"X-Auto-Response-Suppress",
|
||||
"List-Unsubscribe",
|
||||
"List-Unsubscribe-Post",
|
||||
):
|
||||
value = message.get(header)
|
||||
if value is not None:
|
||||
encrypted_message[header] = value
|
||||
|
||||
# Required PGP/MIME version part.
|
||||
version_part = MIMEApplication(
|
||||
b"Version: 1\r\n",
|
||||
_subtype="pgp-encrypted",
|
||||
)
|
||||
version_part.add_header(
|
||||
"Content-Description",
|
||||
"PGP/MIME version identification",
|
||||
)
|
||||
encrypted_message.attach(version_part)
|
||||
|
||||
# Encrypted MIME payload.
|
||||
encrypted_part = MIMEApplication(
|
||||
encrypted_bytes,
|
||||
_subtype="octet-stream",
|
||||
)
|
||||
encrypted_part.add_header(
|
||||
"Content-Description",
|
||||
"OpenPGP encrypted message",
|
||||
)
|
||||
encrypted_part.add_header(
|
||||
"Content-Disposition",
|
||||
"inline",
|
||||
filename="encrypted.asc",
|
||||
)
|
||||
encrypted_message.attach(encrypted_part)
|
||||
|
||||
return encrypted_message
|
||||
|
||||
|
||||
async def create_email_message(
|
||||
to_email: str,
|
||||
subject: str,
|
||||
html_content: str,
|
||||
user_id: int = None,
|
||||
user_id: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Create an HTML email.
|
||||
|
||||
If the recipient has a public GPG key in the database, the complete
|
||||
MIME message is encrypted using PGP/MIME.
|
||||
|
||||
Expected database structure:
|
||||
|
||||
{
|
||||
"email": "recipient@example.com",
|
||||
"gnupg": "-----BEGIN PGP PUBLIC KEY BLOCK-----\\n..."
|
||||
}
|
||||
"""
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
|
||||
msg['From'] = f"{FROM_NAME} <{FROM_EMAIL}>"
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = subject
|
||||
msg['Message-ID'] = make_msgid(domain=FROM_EMAIL.split('@')[1])
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
msg['X-Priority'] = "3"
|
||||
msg['Importance'] = "normal"
|
||||
msg['X-Auto-Response-Suppress'] = "All"
|
||||
|
||||
# token = await create_unsubscribe_token(to_email, user_id)
|
||||
unsubscribe_url = build_unsubscribe_url(to_email, None, FULL_DOMAIN)
|
||||
if to_email != FROM_EMAIL:
|
||||
msg['List-Unsubscribe'] = f"<{unsubscribe_url}>"
|
||||
msg['List-Unsubscribe-Post'] = "List-Unsubscribe=One-Click"
|
||||
|
||||
msg.attach(MIMEText(html_content, 'html'))
|
||||
|
||||
return msg
|
||||
|
||||
msg["From"] = f"{FROM_NAME} <{FROM_EMAIL}>"
|
||||
msg["To"] = to_email
|
||||
msg["Subject"] = subject
|
||||
msg["Message-ID"] = make_msgid(
|
||||
domain=FROM_EMAIL.rsplit("@", 1)[1],
|
||||
)
|
||||
msg["Date"] = formatdate(localtime=True)
|
||||
msg["X-Priority"] = "3"
|
||||
msg["Importance"] = "normal"
|
||||
msg["X-Auto-Response-Suppress"] = "All"
|
||||
|
||||
# Do not send unsubscribe headers to yourself.
|
||||
if to_email.lower() != FROM_EMAIL.lower():
|
||||
unsubscribe_url = build_unsubscribe_url(
|
||||
to_email=to_email,
|
||||
user_id=user_id,
|
||||
domain=FULL_DOMAIN,
|
||||
)
|
||||
|
||||
msg["List-Unsubscribe"] = f"<{unsubscribe_url}>"
|
||||
msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
|
||||
|
||||
msg.attach(
|
||||
MIMEText(
|
||||
html_content,
|
||||
"html",
|
||||
"utf-8",
|
||||
)
|
||||
)
|
||||
|
||||
# Fetch the recipient's public key.
|
||||
user = await users_collection.find_one(
|
||||
{"email": to_email},
|
||||
{"gnupg": 1},
|
||||
)
|
||||
|
||||
public_key = user.get("gnupg") if user else None
|
||||
|
||||
if not public_key:
|
||||
# Return a normal unencrypted email when no key is available.
|
||||
return msg
|
||||
|
||||
if not isinstance(public_key, str):
|
||||
raise TypeError("The gnupg database field must be a string")
|
||||
|
||||
try:
|
||||
fingerprint = import_public_key(public_key)
|
||||
return encrypt_mime_message(msg, fingerprint)
|
||||
|
||||
except Exception:
|
||||
# Encryption is mandatory whenever a key exists.
|
||||
# Do not silently send the message unencrypted.
|
||||
raise
|
||||
|
||||
|
||||
def connect(email_server, email_port):
|
||||
|
||||
@ -38,3 +38,4 @@ celery[redis]==5.6.3
|
||||
pika==1.3.2
|
||||
pillow==12.1.1
|
||||
pytz
|
||||
python-gnupg
|
||||
|
||||
@ -63,6 +63,12 @@
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div class="c_nb6gy floating-group">
|
||||
<label for="gnupg" class="c_z3ybu floating-label">GnuPG</label>
|
||||
<textarea type="text" id="gnupg" name="gnupg" class="c_4ykpn" ></textarea>
|
||||
<span>Самозащита электронной почты</span>
|
||||
</div>
|
||||
|
||||
<div class="c_aihm6 floating-group">
|
||||
<label for="phone" class="c_z3ybu floating-label">Телефон<span style="color:red">*</span></label>
|
||||
<input name="phone" id="phone" class="c_xhd5k" required />
|
||||
@ -222,6 +228,7 @@ document.getElementById('living_place').value = `{% if user.get('living_place')
|
||||
document.getElementById('social_media').value = `{% if user.get('social_media') is not none %}{{user.get('social_media','') }}{% endif %}`
|
||||
document.getElementById('marketplaces').value = `{% if user.get('marketplaces') is not none %}{{user.get('marketplaces','') }}{% endif %}`
|
||||
document.getElementById('email').value = `{% if user.get('email') is not none %}{{user.get('email','') }}{% endif %}`
|
||||
document.getElementById('gnupg').value = `{% if user.get('gnupg') is not none %}{{user.get('gnupg','') }}{% endif %}`
|
||||
document.getElementById('phone').value = `{% if user.get('phone') is not none %}{{user.get('phone','') }}{% endif %}`
|
||||
document.getElementById('messengers').value = `{% if user.get('messengers') is not none %}{{user.get('messengers','') }}{% endif %}`
|
||||
document.getElementById('specialization').value = `{% if user.get('specialization') is not none %}{{user.get('specialization','') }}{% endif %}`
|
||||
@ -270,6 +277,7 @@ document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById(
|
||||
'social_media': document.getElementById('social_media').value,
|
||||
'marketplaces': document.getElementById('marketplaces').value,
|
||||
'email': document.getElementById('email').value,
|
||||
'gnupg': document.getElementById('gnupg').value,
|
||||
'phone': document.getElementById('phone').value,
|
||||
'messengers': document.getElementById('messengers').value,
|
||||
'specialization': document.getElementById('specialization').value,
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
{% import "competition/list.html" as competition %}
|
||||
{% import "components/label.html" as label %}
|
||||
|
||||
{% block title %}Каталог картин{% endblock %}
|
||||
{% block title %}Каталог картин | Выставка {{exhibition.title}}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ header.section(user) }}
|
||||
@ -17,7 +17,7 @@
|
||||
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
||||
<div class="c_3ihvi" id="main">
|
||||
<a class="c_ctfod b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}#main">
|
||||
<span class="c_d2l0b b_al8e7">Отобранные картины</span>
|
||||
<span class="c_d2l0b b_al8e7">Отобранные картины (<span id="lenPosts2">{{ exhibition.posts | length }}</span>)</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="c_tihtk">
|
||||
@ -476,6 +476,8 @@ genre = genres.map(g => {
|
||||
// Обновляем счётчик
|
||||
const lenEl = document.getElementById('lenPosts');
|
||||
if (lenEl) lenEl.textContent = exhibition.posts.length;
|
||||
const lenEl2 = document.getElementById('lenPosts2');
|
||||
if (lenEl2) lenEl2.textContent = exhibition.posts.length;
|
||||
|
||||
} else {
|
||||
el.checked = !isAdding;
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
{% import "components/label.html" as label %}
|
||||
{% import "/components/contacts.html" as contacts %}
|
||||
|
||||
{% block title %}Отобранные картины | Выставка {{exhibition.title}}{% endblock %}
|
||||
{% block title %}Отправить приглашения | Выставка {{exhibition.title}}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user