mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
A send that fails for a passing reason is retried; one that finally fails is marked on the draft (#242)
Every sender turned every failure into `false`, so a one-minute Gmail rate limit or a short outage lost the send like a refused recipient. The three senders now classify through MailDeliveryInterface::deliver(): deferred (rate limits, 5xx, a connection that never opened, SMTP 4xx), refused (other 4xx, SMTP 5xx, too large) or outcome unknown (a lost answer, 502/504). send() stays a bool for invitations, alerts and read receipts. SendMessageHandler retries a deferral for at most two minutes, keeping the provider's Retry-After. A lost answer is never resent blind: Gmail and Graph look for the Message-ID in Sent first (now committed before the first attempt), SMTP reports it as possibly sent. A final failure, from the handler or from SendGaveUpSubscriber when the ladder runs out, marks the draft, words the toast by its reason, and shows a red warning triangle at the start of the draft's row and on Drafts in the sidebar. Migration: four nullable columns and a partial index on message. Closes #125 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d18105909
commit
3b78d277d7
42 files changed
+2305
-89
No files matched your search
@@ -552,6 +552,8 @@ controller instead — the scroll position and which trees are open.
|
||||
<i class="fa-solid fa-file-lines w-4" aria-hidden="true"></i>
|
||||
<span class="rail-hide">{{ 'sidebar.nav.drafts'|trans }}</span>
|
||||
{{ macros.role_dot(enum('App\\Domain\\Enum\\Mail\\LabelRole').Drafts) }}
|
||||
{% import '_partials/_unsent_mark.html.twig' as unsent %}
|
||||
{{ unsent.sidebar(sidebarCounts.unsentDrafts()) }}
|
||||
</span>
|
||||
{{ macros.total_badge(sidebarCounts.forRoleBadge(enum('App\\Domain\\Enum\\Mail\\LabelRole').Drafts), 'role:drafts') }}
|
||||
</a>
|
||||
|
||||
@@ -437,6 +437,16 @@ be a worse regression than the one this feature fixes.
|
||||
different places depending on how wide the list happens to
|
||||
be. #}
|
||||
<span class="min-w-0 flex items-center gap-1.5">
|
||||
{# A send of this draft that finally failed (#125): first in
|
||||
the marker slot, at the very start of the row, where a
|
||||
glance down the Drafts list finds it before anything
|
||||
else. See _unsent_mark.html.twig; `draftMessage` is only
|
||||
set on a conversation row. #}
|
||||
{% if draftMessage|default(null) and draftMessage.sendFailure %}
|
||||
{% import '_partials/_unsent_mark.html.twig' as unsent %}
|
||||
{{ unsent.row(draftMessage.sendFailure) }}
|
||||
{% endif %}
|
||||
|
||||
{# The "New" marker: a filled accent pill carrying a WORD.
|
||||
Deliberately nothing like the unread dot further down
|
||||
this file, which is a bare 8px green circle — the two
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{#
|
||||
The warning on a draft whose send finally failed (#125) — on its row in the
|
||||
Drafts list, and on the Drafts entry in the sidebar while any draft has one.
|
||||
|
||||
One icon in both places, defined once, so the sidebar's mark is plainly the
|
||||
same thing as the row's and the reader knows what to look for when they open
|
||||
Drafts. Red, because it is the one state here that means "this did not happen
|
||||
and you need to know": the toast that said so is gone by the time a scheduled
|
||||
send's failure is noticed.
|
||||
|
||||
The sidebar's is rendered at every count and hidden at zero, the idiom every
|
||||
patched marker follows (see _new_dot.html.twig): ui--sidebar finds it by
|
||||
`data-count-key` after a counts refresh, and an element never rendered cannot
|
||||
be found.
|
||||
#}
|
||||
{% macro row(failure) %}
|
||||
{% set reason = ('thread_row.send_failed.' ~ failure.value)|trans %}
|
||||
<i class="fa-solid fa-triangle-exclamation shrink-0 text-red-600 dark:text-red-400"
|
||||
role="img"
|
||||
data-unsent-draft
|
||||
title="{{ reason }}"
|
||||
aria-label="{{ reason }}"></i>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro sidebar(count) %}
|
||||
<i class="fa-solid fa-triangle-exclamation text-red-600 dark:text-red-400 text-xs{{ count > 0 ? '' : ' hidden' }}"
|
||||
role="img"
|
||||
data-unsent-mark
|
||||
data-count-key="unsent:role:drafts"
|
||||
title="{{ 'sidebar.badge.unsent'|trans }}"
|
||||
aria-label="{{ 'sidebar.badge.unsent'|trans }}"></i>
|
||||
{% endmacro %}
|
||||
@@ -16,14 +16,20 @@ ten. So the confirmation preceded the attempt by two seconds, every time, for
|
||||
everybody, and a send that then failed was never mentioned again.
|
||||
|
||||
Variables: message, sent (bool)
|
||||
|
||||
A failure is worded by its reason (#125), read off the row the handler has just
|
||||
marked: "may have been sent" and "the server refused it" ask different things of
|
||||
the reader. Failed — the reason with nothing more to say — keeps the old words.
|
||||
#}
|
||||
{% set failure = sent ? null : message.sendFailure %}
|
||||
{% set failedKey = failure and failure.value != 'failed' ? 'toast.send_failed_' ~ failure.value : 'toast.send_failed' %}
|
||||
<turbo-stream action="append" target="toast-region">
|
||||
<template>
|
||||
{% include '_partials/_toast.html.twig' with {
|
||||
type: sent ? 'success' : 'error',
|
||||
message: sent
|
||||
? 'toast.sent'|trans
|
||||
: 'toast.send_failed'|trans({ '%subject%': message.subject|default('')|slice(0, 60) }),
|
||||
: failedKey|trans({ '%subject%': message.subject|default('')|slice(0, 60) }),
|
||||
extra: sent ? {} : { hide_after: 12000 },
|
||||
} only %}
|
||||
</template>
|
||||
|
||||
Reference in new issue
Block a user