Skip to content

Commit 0c26a66

Browse files
authored
[POSTMAN-UNSUPPORTED-ATTACHMENT-2] PLU-491: Retry without attachments (#1112)
## TL;DR This update enhances the retry functionality for Email by Postman actions by allowing users to retry sending emails without attachments, in the following cases: • Old executions that failed due to unsupported and/or password-protected attachments • New executions that fail due to password-protected attachments ## Why make this change? Currently, if an email fails due to unsupported or password-protected attachments, users are unable to successfully retry sending it—even if the content is otherwise valid. This change introduces an option for users to resend the email **without any attachments**, enabling successful delivery. **Why remove all attachments if there are password-protected attachments?** Postman returns the same error for both unsupported and password-protected attachments, and we cannot reliably identify which specific file is password-protected. As a result, all attachments must be removed to ensure the retry succeeds. ## How to test? _bear with me, there are many scenarios to test_ 😅 ### Existing failed executions Need to trigger these failed executions while on `develop-v2` first - [x] **Blacklisted**: retry sends as intended (current behaviour) - [x] **Blacklisted + password-protected**: - [x] if email is still blacklisted, sends email without any attachments - [x] once email is whitelisted, retry sends without any attachment - [x] **Blacklisted + unsupported**: retry sends without any attachment - [x] if email is still blacklisted, sends email without any attachments - [x] once email is whitelisted, retry sends without any attachment - [x] **Blacklisted + password-protected + unsupported**: retry sends without any attachment - [x] if email is still blacklisted, sends email without any attachments - [x] once email is whitelisted, retry sends without any attachment - [x] **Password-protected**: retry sends without any attachment - [x] **Password-protected + unsupported**: retry sends without any attachment - [x] **Unsupported**: retry sends without any attachment ### New executions using the pre-emptive filter These failed executions can be triggered while on this branch - [x] **Blacklisted**: retry sends as intended (current behaviour) - [x] Includes all supported attachments - [x] **Blacklisted + password-protected**: - [x] if still blacklisted, retry sends without any attachment - [x] once email is whitelisted, retry sends without any attachment - [x] **Blacklisted + unsupported**: retry sends without the _unsupported_ attachment - [x] Non-blacklisted recipients would have received all other attachments less the _unsupported_ one - [x] Retry button should say `Resend to blacklisted recipients` - [x] Retry sends email without the _unsupported_ attachment to the whitelisted email - [x] **Blacklisted + password-protected + unsupported** - [x] if still blacklisted, retry sends without any attachment - [x] once email is whitelisted, retry sends without any attachment - [x] **Password-protected**: retry sends without any attachment - [x] **Password-protected + unsupported**: retry sends without any attachment
1 parent f8f02ff commit 0c26a66

File tree

5 files changed

+153
-28
lines changed

5 files changed

+153
-28
lines changed

packages/backend/src/apps/postman/actions/send-transactional-email/index.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,6 @@ const action: IRawAction = {
7474
)
7575
}
7676

77-
const { attachmentFiles, invalidAttachments, submissionId } =
78-
await filterAttachments(result.data.attachments, $)
79-
8077
let recipientsToSend = result.data.destinationEmail
8178
/**
8279
* Logic to handle retries here:
@@ -99,6 +96,18 @@ const action: IRawAction = {
9996
// Don't do partial retry in test runs! always send to all recipients
10097
!$.execution.testRun
10198

99+
const {
100+
attachmentFiles,
101+
invalidAttachments,
102+
submissionId,
103+
isRetryWithoutAttachments,
104+
} = await filterAttachments({
105+
$,
106+
attachmentsList: result.data.attachments,
107+
isPartialRetry,
108+
lastExecutionStep,
109+
})
110+
102111
if (isPartialRetry) {
103112
const { status, recipient } = prevDataOutParseResult.data
104113
recipientsToSend = recipient.filter((_, i) => status[i] !== 'ACCEPTED')
@@ -196,8 +205,14 @@ const action: IRawAction = {
196205
/**
197206
* Send invalid attachments notification email
198207
* Do not send on partial retry as we would have already sent this once with the blacklist email
208+
* Do not send on retry when removing all attachments
199209
*/
200-
if (hasInvalidAttachments && !isPartialRetry && !$.execution.testRun) {
210+
if (
211+
hasInvalidAttachments &&
212+
!isPartialRetry &&
213+
!$.execution.testRun &&
214+
!isRetryWithoutAttachments
215+
) {
201216
await sendInvalidAttachmentsEmail({
202217
...defaultSendEmailParams,
203218
...invalidAttachmentParams,
@@ -222,6 +237,7 @@ const action: IRawAction = {
222237
isPartialSuccess: hasAtLeastOneSuccess || invalidAttachments.length > 0,
223238
blacklistedRecipients,
224239
invalidAttachments,
240+
isRetryWithoutAttachments,
225241
})
226242
}
227243
},

packages/backend/src/apps/postman/common/parameters-helper.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IGlobalVariable } from '@plumber/types'
1+
import { IExecutionStep, IGlobalVariable, IJSONObject } from '@plumber/types'
22

33
import { COMMON_S3_BUCKET, getObjectFromS3Id } from '@/helpers/s3'
44
import Flow from '@/models/flow'
@@ -13,21 +13,63 @@ export async function getDefaultReplyTo(flowId: string): Promise<string> {
1313
return flow.user.email
1414
}
1515

16-
export async function filterAttachments(
17-
attachmentsList: string[],
18-
$: IGlobalVariable,
19-
) {
16+
export async function filterAttachments({
17+
$,
18+
attachmentsList,
19+
isPartialRetry,
20+
lastExecutionStep,
21+
}: {
22+
$: IGlobalVariable
23+
attachmentsList: string[]
24+
isPartialRetry: boolean
25+
lastExecutionStep: IExecutionStep | null
26+
}) {
2027
let submissionId: string | null = null
2128
const invalidAttachments: string[] = []
2229
const attachmentFiles: { fileName: string; data: Uint8Array }[] = []
2330

31+
const errorName = lastExecutionStep?.errorDetails?.name
32+
const partialRetryButtonMessage = (
33+
lastExecutionStep?.errorDetails?.partialRetry as IJSONObject
34+
)?.buttonMessage
35+
36+
/**
37+
* NOTE: there are different scenarios where we should retry without attachments:
38+
* 1. Password-protected attachment(s)
39+
* Postman does not tell us reliably which attachment(s) are password-protected
40+
* so we remove all attachments instead.
41+
*
42+
* 2. Unsupported attachment file type
43+
* This is the old error code that is thrown when there are unsupported or password-protected attachments.
44+
* We remove all attachments when retrying old executions to avoid having to retry twice in the event
45+
* that there are password-protected attachments, which will cause another failure.
46+
*
47+
* 3. isPartialRetry and Resend to blacklisted recipients without attachments
48+
* This is to handle executions that failed due to invalid attachments and had blacklisted recipients.
49+
* Upon retry, the execution runs without attachments, but the blacklisted recipient still exists.
50+
* In such scenarios, we show the partial retry button with 'Resend to blacklisted recipients without attachments',
51+
* and use this to tell the worker to remove all attachments.
52+
*/
53+
const isRetryWithoutAttachments =
54+
errorName === 'Password-protected attachment(s)' ||
55+
errorName === 'Unsupported attachment file type' ||
56+
(isPartialRetry &&
57+
partialRetryButtonMessage ===
58+
'Resend to blacklisted recipients without attachments')
59+
2460
await Promise.all(
2561
attachmentsList?.map(async (attachment) => {
2662
// We verify the flowId here to ensure that the attachment is from the same flow and not
2763
// maliciously/ manually injected by another user who does not have access to this attachment
2864
const obj = await getObjectFromS3Id(attachment, { flowId: $.flow.id }, $)
2965
const fileName = obj.name
3066
const fileType = obj.name.split('.').pop()?.toLowerCase()
67+
68+
if (isRetryWithoutAttachments) {
69+
invalidAttachments.push(fileName)
70+
return
71+
}
72+
3173
if (!fileType || !POSTMAN_ACCEPTED_EXTENSIONS.includes(fileType)) {
3274
invalidAttachments.push(fileName)
3375

@@ -43,5 +85,20 @@ export async function filterAttachments(
4385
return
4486
}),
4587
)
46-
return { attachmentFiles, invalidAttachments, submissionId }
88+
89+
if (isRetryWithoutAttachments) {
90+
return {
91+
attachmentFiles: [],
92+
invalidAttachments,
93+
submissionId,
94+
isRetryWithoutAttachments,
95+
}
96+
}
97+
98+
return {
99+
attachmentFiles,
100+
invalidAttachments,
101+
submissionId,
102+
isRetryWithoutAttachments,
103+
}
47104
}

packages/backend/src/apps/postman/common/throw-errors.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,23 @@ export function getPostmanErrorStatus(
4747
}
4848
}
4949

50-
function getInvalidAttachmentSolution(invalidAttachments: string[]) {
51-
return `The following attachment(s) are not supported by Postman and have been removed from the email:
52-
\n${invalidAttachments.map((attachment) => `**${attachment}**`).join('\n\n')}
53-
\nIf you require the attachment(s), log in to your form to download them for this submission.
50+
function getInvalidAttachmentSolution({
51+
invalidAttachments,
52+
showAttachmentsList = true,
53+
}: {
54+
invalidAttachments: string[]
55+
showAttachmentsList?: boolean
56+
}) {
57+
if (showAttachmentsList) {
58+
return `The following attachment(s) are not supported by Postman and have been removed from the email:
59+
\n${invalidAttachments
60+
.map((attachment) => `**${attachment}**`)
61+
.join('\n\n')}
62+
\nIf you require the attachment(s), log in to your form to download them for this submission.
63+
`
64+
}
65+
return `There were attachment(s) that could not be sent by Postman and have been removed from the email.
66+
\nIf you require the attachment(s), log in to your form to download them for this submission.
5467
`
5568
}
5669

@@ -61,20 +74,25 @@ export function throwPostmanStepError({
6174
isPartialSuccess,
6275
blacklistedRecipients,
6376
invalidAttachments,
77+
isRetryWithoutAttachments,
6478
}: {
6579
$: IGlobalVariable
6680
status: PostmanEmailSendStatus
6781
error: HttpError
6882
isPartialSuccess: boolean
6983
blacklistedRecipients: string[]
7084
invalidAttachments: string[]
85+
isRetryWithoutAttachments: boolean
7186
}) {
7287
const position = $.step.position
7388
const appName = $.app.name
7489

7590
const hasInvalidAttachments = invalidAttachments.length > 0
76-
const invalidAttachmentsSolution =
77-
getInvalidAttachmentSolution(invalidAttachments)
91+
const invalidAttachmentsSolution = getInvalidAttachmentSolution({
92+
invalidAttachments,
93+
// should not show attachments list if we are retrying without attachments
94+
showAttachmentsList: !isRetryWithoutAttachments,
95+
})
7896

7997
switch (status) {
8098
case 'BLACKLISTED': {
@@ -96,14 +114,19 @@ export function throwPostmanStepError({
96114
solution += `\n\n&nbsp;\n\n${invalidAttachmentsSolution}`
97115
}
98116

117+
let buttonMessage = 'Resend to blacklisted recipients'
118+
if (isRetryWithoutAttachments) {
119+
buttonMessage += ' without attachments'
120+
}
121+
99122
if (isPartialSuccess) {
100123
throw new PartialStepError({
101124
name,
102125
solution,
103126
position,
104127
appName,
105128
partialRetry: {
106-
buttonMessage: 'Resend to blacklisted recipients',
129+
buttonMessage,
107130
},
108131
})
109132
}
@@ -152,7 +175,10 @@ export function throwPostmanStepError({
152175
// attachments that are password-protected
153176
if (hasInvalidAttachments) {
154177
const name = 'Invalid attachment(s)'
155-
const solution = getInvalidAttachmentSolution(invalidAttachments)
178+
const solution = getInvalidAttachmentSolution({
179+
invalidAttachments,
180+
showAttachmentsList: true,
181+
})
156182

157183
// throw StepError for test runs so that user cannot publish the pipe
158184
// until the error is fixed
@@ -162,7 +188,7 @@ export function throwPostmanStepError({
162188

163189
throw new PartialStepError({
164190
name,
165-
solution,
191+
solution: invalidAttachmentsSolution,
166192
position,
167193
appName,
168194
partialRetry: { buttonMessage: '' }, // nothing to retry

packages/frontend/src/components/ExecutionStep/hooks/useExecutionStepStatus.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface UseExecutionStepStatusReturn {
3030
isPartialSuccess: boolean
3131
canRetry: boolean
3232
loading: boolean
33+
customRetryButtonText?: string
3334
}
3435

3536
export function useExecutionStepStatus({
@@ -63,6 +64,20 @@ export function useExecutionStepStatus({
6364
return failureIcon
6465
}, [isPartialSuccess, isStepSuccessful, status])
6566

67+
const customRetryButtonText = useMemo(() => {
68+
// specific to email by postman where we want to allow users to retry without attachments
69+
// postman returns this error code when:
70+
// - attachment is password-protected; AND/OR
71+
// - attachment is unsupported
72+
if (
73+
errorDetails?.name === 'Unsupported attachment file type' ||
74+
errorDetails?.name === 'Password-protected attachment(s)'
75+
) {
76+
return 'Retry without attachments'
77+
}
78+
return undefined
79+
}, [errorDetails])
80+
6681
return {
6782
app,
6883
appName,
@@ -72,5 +87,6 @@ export function useExecutionStepStatus({
7287
isPartialSuccess,
7388
canRetry,
7489
loading,
90+
customRetryButtonText,
7591
}
7692
}

packages/frontend/src/components/ExecutionStep/index.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,21 @@ export default function ExecutionStep({
4242
}: ExecutionStepProps): React.ReactElement | null {
4343
const { appKey, jobId, errorDetails, status } = executionStep
4444

45-
const { app, appName, statusIcon, hasError, isPartialSuccess, canRetry } =
46-
useExecutionStepStatus({
47-
appKey,
48-
status,
49-
errorDetails,
50-
execution,
51-
jobId,
52-
})
45+
const {
46+
app,
47+
appName,
48+
statusIcon,
49+
hasError,
50+
isPartialSuccess,
51+
canRetry,
52+
customRetryButtonText,
53+
} = useExecutionStepStatus({
54+
appKey,
55+
status,
56+
errorDetails,
57+
execution,
58+
jobId,
59+
})
5360

5461
if (!app) {
5562
return null
@@ -77,7 +84,10 @@ export default function ExecutionStep({
7784
{!isInForEach && canRetry && (
7885
<>
7986
<RetryAllButton execution={execution} />
80-
<RetryButton executionStepId={executionStep.id} />
87+
<RetryButton
88+
executionStepId={executionStep.id}
89+
customButtonText={customRetryButtonText}
90+
/>
8191
</>
8292
)}
8393
</HStack>

0 commit comments

Comments
 (0)