From 9fc280fd7ba01575cc5b7b97b189f5a367cbfaf4 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 14 May 2026 17:30:02 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20email=20images=20=E2=80=94=20propagate?= =?UTF-8?q?=20shop=5Fcdn=5Fendpoint=20+=20thumbnails=20in=20offers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coupled fixes the rendered sale email exposed: 1. ShopContextRequestWrapper (lib/crypto_watcher/__init__.py:1243) wraps env_request for email-rendering inside the watcher loop. It overrode domain / host_url / app and proxied everything else via __getattr__. send_purchase_email + send_sale_email read `request.shop_cdn_endpoint` — but that's a reified Pyramid request method, not a static attr on env_request. __getattr__'s default returned None, and the email's became `None/.../...`. Fix: add explicit `shop_cdn_endpoint` (and `shop`) properties on the wrapper, derived from the wrapper's `_shop`. Order: - BYOB shop with primary_s3_cdn_endpoint set → that - else `app["bucket.secure_uploads.get_endpoint"]` (MPS default) - else None The BYOB branch also defends against an enabled-but-blank primary_s3_cdn_endpoint — drops to the default rather than returning None. 2. Offer + auction-outbid emails carried no product thumbnail at all — bummer, since the recipient can't visually identify which item the negotiation is about. New _product_thumbnail_html(request, product) helper renders a 184px-max identical to the purchase/sale shape (or empty string if the product has no thumbnail1 extension / no CDN endpoint). Wired into: - send_offer_received_email - send_offer_accepted_email - send_offer_countered_email - send_offer_declined_email - send_offer_withdrawn_email - send_offer_buyer_cancelled_email - send_auction_outbid_email Templates in lib/mail_messages.py gained a `{thumbnail}` slot between the headline and the click-through link. Text variants are unchanged (no inline images in text email). Both fixes target the same surface: every transactional email now renders the right image, regardless of whether it's sent from a view (Pyramid request) or the crypto watcher loop (wrapped env_req). --- make_post_sell/lib/crypto_watcher/__init__.py | 30 ++++++++++++++++ make_post_sell/lib/mail.py | 34 +++++++++++++++++++ make_post_sell/lib/mail_messages.py | 7 ++++ 3 files changed, 71 insertions(+) diff --git a/make_post_sell/lib/crypto_watcher/__init__.py b/make_post_sell/lib/crypto_watcher/__init__.py index 3d14713..ad3e11d 100644 --- a/make_post_sell/lib/crypto_watcher/__init__.py +++ b/make_post_sell/lib/crypto_watcher/__init__.py @@ -1275,6 +1275,36 @@ class ShopContextRequestWrapper: """Proxy app attribute from original request.""" return getattr(self._original_request, "app", {}) + @property + def shop(self): + """The shop this wrapped request is scoped to. Used by + derivative properties (e.g. shop_cdn_endpoint) that would + otherwise return None — env_request from the watcher loop + doesn't carry a request.shop.""" + return self._shop + + @property + def shop_cdn_endpoint(self): + """Mirror of the regular `request.shop_cdn_endpoint` reified + method (see request_methods.py:add_shop_cdn_endpoint). Used by + send_purchase_email + send_sale_email to build product + thumbnail URLs. Without this, the proxy fell through to None + and the email's rendered as `None//...`.""" + shop = self._shop + # BYOB shop with an explicit CDN endpoint wins. + if shop is not None and getattr(shop, "has_primary_s3", False): + byob = shop.primary_s3_cdn_endpoint + if byob: + return byob + # Otherwise fall back to the MPS-default public CDN. + app = getattr(self._original_request, "app", None) + if app is None: + return None + try: + return app["bucket.secure_uploads.get_endpoint"] + except Exception: + return None + def create_shop_context_request(env_request, crypto_payment: CryptoPayment): """Create a request wrapper with shop domain context for email generation.""" diff --git a/make_post_sell/lib/mail.py b/make_post_sell/lib/mail.py index a561225..22dda81 100644 --- a/make_post_sell/lib/mail.py +++ b/make_post_sell/lib/mail.py @@ -632,10 +632,38 @@ def send_auction_outbid_email(request, to_email, auction): title=auction.product.title, high=f"{auction.current_high:.2f}", auction_url=auction_url, + thumbnail=_product_thumbnail_html(request, auction.product), ) send_pyramid_email(request, to_email, subject, text, html) +def _product_thumbnail_html(request, product): + """Render a tokenized thumbnail tag for inclusion in a + transactional email body, or empty string if the product has no + thumbnail1 extension uploaded. + + Sized for 184px max — same dimensions as the purchase/sale emails + use, so the recipient sees a consistent image card across the whole + email surface. Returns "" (not None) so format calls can splice + without conditional logic. + """ + if product is None or "thumbnail1" not in getattr(product, "extensions", []): + return "" + cdn = getattr(request, "shop_cdn_endpoint", None) + if not cdn: + return "" + return ( + '' + ).format( + cdn=cdn, + path=product.s3_path, + ts=product.updated_timestamp, + ) + + def send_offer_received_email(request, to_email, offer): """Notify a shop owner that a new offer arrived for review.""" offer_url = f"{request.host_url}/o/{offer.uuid_str}" @@ -649,6 +677,7 @@ def send_offer_received_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email(request, to_email, subject, text, html) @@ -666,6 +695,7 @@ def send_offer_accepted_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email(request, to_email, subject, text, html) @@ -680,6 +710,7 @@ def send_offer_countered_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, @@ -698,6 +729,7 @@ def send_offer_declined_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, @@ -716,6 +748,7 @@ def send_offer_withdrawn_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, @@ -734,6 +767,7 @@ def send_offer_buyer_cancelled_email(request, to_email, offer): amount=f"{offer.current_amount:.2f}", title=offer.product.title, offer_url=offer_url, + thumbnail=_product_thumbnail_html(request, offer.product), ) send_pyramid_email( request, to_email, subject, diff --git a/make_post_sell/lib/mail_messages.py b/make_post_sell/lib/mail_messages.py index e4cb6c3..a574fd7 100644 --- a/make_post_sell/lib/mail_messages.py +++ b/make_post_sell/lib/mail_messages.py @@ -360,6 +360,7 @@ AUCTION_OUTBID_HTML = """

You have been outbid

Someone outbid you on {title}.

+{thumbnail}

Current high: ${high}.

Place a new bid

@@ -381,6 +382,7 @@ OFFER_RECEIVED_HTML = """

New offer received

You received a new offer of ${amount} for {title}.

+{thumbnail}

View offer

@@ -401,6 +403,7 @@ OFFER_ACCEPTED_HTML = """

Offer accepted

Your offer of ${amount} for {title} was accepted.

+{thumbnail}

Pay now

@@ -421,6 +424,7 @@ OFFER_COUNTERED_HTML = """

Counter offer received

The other party countered with ${amount} for {title}.

+{thumbnail}

Review and respond

@@ -441,6 +445,7 @@ OFFER_DECLINED_HTML = """

Offer declined

Your offer of ${amount} for {title} was declined.

+{thumbnail}

View details

@@ -461,6 +466,7 @@ OFFER_WITHDRAWN_HTML = """

Offer withdrawn

The buyer withdrew their offer of ${amount} for {title}.

+{thumbnail}

View details

@@ -482,6 +488,7 @@ OFFER_BUYER_CANCELLED_HTML = """

Buyer cancelled accepted offer

The buyer cancelled their accepted offer of ${amount} for {title}.

+{thumbnail}

You accepted this offer earlier but the buyer backed out before paying.

View details