forked from retoor/devplacepy
docs: document server-side rendering pipeline, response timing middleware, and Telegram pairing API
- Add comprehensive documentation for backend content rendering in AGENTS.md, detailing the new `render_content` and `render_title` Jinja globals built on mistune with media processing, emoji shortcodes, and XSS protection
- Document the `X-Response-Time` header and bottom-left render time indicator in README.md
- Update bot token pricing documentation to clarify fallback vs gateway cost headers
- Add `email_accounts` to soft-delete tables and `idx_users_role` composite index in database schema
- Implement `telegram_pairings` and `telegram_links` table creation with column migration and indexes
- Add `/profile/{username}/telegram` endpoint to docs API with request/unpair actions
- Register `TelegramService` in main.py lifespan and add `response_timing` middleware emitting `X-Response-Time` header
- Introduce `TelegramPairForm` model and `guard_public_host_sync` synchronous host validation function
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -0,0 +1,113 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from email.message import EmailMessage
|
||||
|
||||
import pytest
|
||||
|
||||
from devplacepy.services.email.client import (
|
||||
EmailClient,
|
||||
_decode_date,
|
||||
_header_addresses,
|
||||
imap_quote,
|
||||
)
|
||||
from devplacepy.services.email.config import EmailAccount
|
||||
from devplacepy.services.email.errors import EmailError
|
||||
|
||||
|
||||
def _account(**overrides):
|
||||
base = {
|
||||
"label": "work",
|
||||
"imap_host": "127.0.0.1",
|
||||
"imap_port": 993,
|
||||
"imap_ssl": 1,
|
||||
"smtp_host": "127.0.0.1",
|
||||
"smtp_port": 587,
|
||||
"smtp_starttls": 1,
|
||||
"username": "me@example.com",
|
||||
"password": "secret",
|
||||
}
|
||||
base.update(overrides)
|
||||
return EmailAccount.from_row(base)
|
||||
|
||||
|
||||
def test_imap_quote_escapes_quotes_and_backslashes():
|
||||
assert imap_quote('a"b\\c') == '"a\\"b\\\\c"'
|
||||
|
||||
|
||||
def test_header_addresses_extracts_only_addresses():
|
||||
parsed = _header_addresses("Alice <a@x.dev>, b@y.dev")
|
||||
assert parsed == ["Alice <a@x.dev>", "b@y.dev"]
|
||||
|
||||
|
||||
def test_header_addresses_empty_is_empty_list():
|
||||
assert _header_addresses("") == []
|
||||
|
||||
|
||||
def test_decode_date_parses_rfc2822():
|
||||
iso = _decode_date("Wed, 18 Jun 2025 10:30:00 +0000")
|
||||
assert iso is not None and iso.startswith("2025-06-18T10:30:00")
|
||||
|
||||
|
||||
def test_decode_date_returns_raw_on_unparseable():
|
||||
assert _decode_date("not a date") == "not a date"
|
||||
|
||||
|
||||
def test_decode_date_empty_is_none():
|
||||
assert _decode_date("") is None
|
||||
|
||||
|
||||
def test_imap_without_host_raises_config_error():
|
||||
client = EmailClient(_account(imap_host=""))
|
||||
with pytest.raises(EmailError) as exc:
|
||||
client.list_folders()
|
||||
assert exc.value.kind == "config"
|
||||
|
||||
|
||||
def test_smtp_without_host_raises_config_error():
|
||||
client = EmailClient(_account(smtp_host=""))
|
||||
with pytest.raises(EmailError) as exc:
|
||||
client.send_message(to=["x@y.dev"], subject="hi", body="b")
|
||||
assert exc.value.kind == "config"
|
||||
|
||||
|
||||
def test_imap_loopback_host_is_blocked():
|
||||
client = EmailClient(_account(imap_host="127.0.0.1"))
|
||||
with pytest.raises(EmailError) as exc:
|
||||
client.list_folders()
|
||||
assert exc.value.kind == "blocked"
|
||||
|
||||
|
||||
def test_smtp_loopback_host_is_blocked():
|
||||
client = EmailClient(_account(smtp_host="127.0.0.1"))
|
||||
with pytest.raises(EmailError) as exc:
|
||||
client.send_message(to=["x@y.dev"], subject="hi", body="b")
|
||||
assert exc.value.kind == "blocked"
|
||||
|
||||
|
||||
def test_render_message_truncates_oversized_body():
|
||||
client = EmailClient(_account())
|
||||
message = EmailMessage()
|
||||
message["From"] = "Alice <a@x.dev>"
|
||||
message["To"] = "b@y.dev"
|
||||
message["Subject"] = "topic"
|
||||
message.set_content("x" * 200_000)
|
||||
rendered = client._render_message("17", "INBOX", ["\\Seen"], message)
|
||||
assert rendered["body_truncated"] is True
|
||||
assert len(rendered["body"]) == 80_000
|
||||
assert rendered["uid"] == "17"
|
||||
assert rendered["to"] == ["b@y.dev"]
|
||||
|
||||
|
||||
def test_render_message_lists_attachments():
|
||||
client = EmailClient(_account())
|
||||
message = EmailMessage()
|
||||
message["From"] = "a@x.dev"
|
||||
message["Subject"] = "with attachment"
|
||||
message.set_content("body")
|
||||
message.add_attachment(
|
||||
b"payload", maintype="application", subtype="pdf", filename="report.pdf"
|
||||
)
|
||||
rendered = client._render_message("3", "INBOX", [], message)
|
||||
assert rendered["body"] == "body"
|
||||
assert rendered["attachments"][0]["filename"] == "report.pdf"
|
||||
assert rendered["attachments"][0]["content_type"] == "application/pdf"
|
||||
@@ -0,0 +1,59 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.email.config import EmailAccount
|
||||
|
||||
|
||||
def test_defaults_apply_imap_993_and_smtp_587():
|
||||
account = EmailAccount.from_row({"username": "me@example.com"})
|
||||
assert account.imap_port == 993
|
||||
assert account.smtp_port == 587
|
||||
|
||||
|
||||
def test_from_address_falls_back_to_username():
|
||||
account = EmailAccount.from_row({"username": "me@example.com"})
|
||||
assert account.from_address == "me@example.com"
|
||||
|
||||
|
||||
def test_explicit_from_address_wins():
|
||||
account = EmailAccount.from_row(
|
||||
{"username": "login@example.com", "from_address": "public@example.com"}
|
||||
)
|
||||
assert account.from_address == "public@example.com"
|
||||
|
||||
|
||||
def test_boolean_coercion_from_strings():
|
||||
account = EmailAccount.from_row(
|
||||
{
|
||||
"imap_ssl": "true",
|
||||
"imap_starttls": "0",
|
||||
"smtp_ssl": "yes",
|
||||
"smtp_starttls": "on",
|
||||
}
|
||||
)
|
||||
assert account.imap_ssl is True
|
||||
assert account.imap_starttls is False
|
||||
assert account.smtp_ssl is True
|
||||
assert account.smtp_starttls is True
|
||||
|
||||
|
||||
def test_boolean_coercion_from_integers():
|
||||
account = EmailAccount.from_row({"imap_ssl": 1, "smtp_ssl": 0})
|
||||
assert account.imap_ssl is True
|
||||
assert account.smtp_ssl is False
|
||||
|
||||
|
||||
def test_port_coercion_falls_back_on_garbage():
|
||||
account = EmailAccount.from_row({"imap_port": "not-a-port", "smtp_port": "2525"})
|
||||
assert account.imap_port == 993
|
||||
assert account.smtp_port == 2525
|
||||
|
||||
|
||||
def test_missing_fields_become_empty_strings():
|
||||
account = EmailAccount.from_row({})
|
||||
assert account.label == ""
|
||||
assert account.imap_host == ""
|
||||
assert account.smtp_host == ""
|
||||
assert account.username == ""
|
||||
assert account.password == ""
|
||||
assert account.from_address == ""
|
||||
assert account.from_name == ""
|
||||
Reference in New Issue
Block a user