Skip to content

Commit 18ec7c1

Browse files
committed
chore: retry emails without attachments
1 parent 8bece6d commit 18ec7c1

File tree

5 files changed

+130
-35
lines changed

5 files changed

+130
-35
lines changed

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

Lines changed: 21 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, formId } =
78-
await filterAttachments(result.data.attachments, $)
79-
8077
let recipientsToSend = result.data.destinationEmail
8178
/**
8279
* Logic to handle retries here:
@@ -99,6 +96,19 @@ 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+
formId,
104+
isRetryWithoutAttachments,
105+
} = await filterAttachments({
106+
$,
107+
attachmentsList: result.data.attachments,
108+
isPartialRetry,
109+
lastExecutionStep,
110+
})
111+
102112
if (isPartialRetry) {
103113
const { status, recipient } = prevDataOutParseResult.data
104114
recipientsToSend = recipient.filter((_, i) => status[i] !== 'ACCEPTED')
@@ -198,8 +208,14 @@ const action: IRawAction = {
198208
/**
199209
* Send invalid attachments notification email
200210
* Do not send on partial retry as we would have already sent this once with the blacklist email
211+
* Do not send on retry when removing all attachments
201212
*/
202-
if (hasInvalidAttachments && !isPartialRetry && !$.execution.testRun) {
213+
if (
214+
hasInvalidAttachments &&
215+
!isPartialRetry &&
216+
!$.execution.testRun &&
217+
!isRetryWithoutAttachments
218+
) {
203219
await sendInvalidAttachmentsEmail({
204220
...defaultSendEmailParams,
205221
...invalidAttachmentParams,
@@ -225,6 +241,7 @@ const action: IRawAction = {
225241
blacklistedRecipients,
226242
invalidAttachments,
227243
formAdminLink,
244+
isRetryWithoutAttachments,
228245
})
229246
}
230247
},

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

Lines changed: 66 additions & 11 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 ExecutionStep from '@/models/execution-step'
@@ -24,28 +24,66 @@ async function getFormId(executionId: string) {
2424
return String(formData?.dataOut?.formId)
2525
}
2626

27-
export async function filterAttachments(
28-
attachmentsList: string[],
29-
$: IGlobalVariable,
30-
) {
31-
let formId: string | null = null
27+
export async function filterAttachments({
28+
$,
29+
attachmentsList,
30+
isPartialRetry,
31+
lastExecutionStep,
32+
}: {
33+
$: IGlobalVariable
34+
attachmentsList: string[]
35+
isPartialRetry: boolean
36+
lastExecutionStep: IExecutionStep | null
37+
}) {
38+
const formId: string | null = await getFormId($.execution.id)
3239
let submissionId: string | null = null
3340
const invalidAttachments: string[] = []
3441
const attachmentFiles: { fileName: string; data: Uint8Array }[] = []
3542

43+
const errorName = lastExecutionStep?.errorDetails?.name
44+
const partialRetryButtonMessage = (
45+
lastExecutionStep?.errorDetails?.partialRetry as IJSONObject
46+
)?.buttonMessage
47+
48+
/**
49+
* NOTE: there are different scenarios where we should retry without attachments:
50+
* 1. Password-protected attachment(s)
51+
* Postman does not tell us reliably which attachment(s) are password-protected
52+
* so we remove all attachments instead.
53+
*
54+
* 2. Unsupported attachment file type
55+
* This is the old error code that is thrown when there are unsupported or password-protected attachments.
56+
* We remove all attachments when retrying old executions to avoid having to retry twice in the event
57+
* that there are password-protected attachments, which will cause another failure.
58+
*
59+
* 3. isPartialRetry and Resend to blacklisted recipients without attachments
60+
* This is to handle executions that failed due to invalid attachments and had blacklisted recipients.
61+
* Upon retry, the execution runs without attachments, but the blacklisted recipient still exists.
62+
* In such scenarios, we show the partial retry button with 'Resend to blacklisted recipients without attachments',
63+
* and use this to tell the worker to remove all attachments.
64+
*/
65+
const isRetryWithoutAttachments =
66+
errorName === 'Password-protected attachment(s)' ||
67+
errorName === 'Unsupported attachment file type' ||
68+
(isPartialRetry &&
69+
partialRetryButtonMessage ===
70+
'Resend to blacklisted recipients without attachments')
71+
3672
await Promise.all(
3773
attachmentsList?.map(async (attachment) => {
3874
// We verify the flowId here to ensure that the attachment is from the same flow and not
3975
// maliciously/ manually injected by another user who does not have access to this attachment
4076
const obj = await getObjectFromS3Id(attachment, { flowId: $.flow.id }, $)
4177
const fileName = obj.name
4278
const fileType = obj.name.split('.').pop()
43-
if (!POSTMAN_ACCEPTED_EXTENSIONS.includes(fileType)) {
79+
80+
if (isRetryWithoutAttachments) {
4481
invalidAttachments.push(fileName)
82+
return
83+
}
4584

46-
if (formId === null) {
47-
formId = await getFormId($.execution.id)
48-
}
85+
if (!POSTMAN_ACCEPTED_EXTENSIONS.includes(fileType)) {
86+
invalidAttachments.push(fileName)
4987

5088
if (submissionId === null) {
5189
submissionId = attachment.slice(
@@ -59,5 +97,22 @@ export async function filterAttachments(
5997
return
6098
}),
6199
)
62-
return { attachmentFiles, invalidAttachments, submissionId, formId }
100+
101+
if (isRetryWithoutAttachments) {
102+
return {
103+
attachmentFiles: [],
104+
invalidAttachments,
105+
submissionId,
106+
formId,
107+
isRetryWithoutAttachments,
108+
}
109+
}
110+
111+
return {
112+
attachmentFiles,
113+
invalidAttachments,
114+
submissionId,
115+
formId,
116+
isRetryWithoutAttachments,
117+
}
63118
}

packages/backend/src/apps/postman/common/send-invalid-attachments-email.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function sendInvalidAttachmentsEmail(
6262
) {
6363
const { flowName, userEmail, formAdminLink } = props
6464
const truncatedFlowName = truncateFlowName(flowName)
65-
const bodyContent = await createInvalidAttachmentsMessage({
65+
const bodyContent = createInvalidAttachmentsMessage({
6666
...props,
6767
formAdminLink,
6868
})

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,22 @@ export function getPostmanErrorStatus(
5050
function getInvalidAttachmentSolution({
5151
invalidAttachments,
5252
formAdminLink,
53+
showAttachmentsList = true,
5354
}: {
5455
invalidAttachments: string[]
5556
formAdminLink: string | null
57+
showAttachmentsList?: boolean
5658
}) {
57-
return `The following attachment(s) are not supported by Postman and have been removed from the email:
58-
\n${invalidAttachments.map((attachment) => `**${attachment}**`).join('\n\n')}
59-
\nIf you require the attachment(s), log in to your [form](${formAdminLink}) to download them for this submission.
59+
if (showAttachmentsList) {
60+
return `The following attachment(s) are not supported by Postman and have been removed from the email:
61+
\n${invalidAttachments
62+
.map((attachment) => `**${attachment}**`)
63+
.join('\n\n')}
64+
\nIf you require the attachment(s), log in to your [form](${formAdminLink}) to download them for this submission.
65+
`
66+
}
67+
return `There were attachment(s) that could not be sent by Postman and have been removed from the email.
68+
\nIf you require the attachment(s), log in to your [form](${formAdminLink}) to download them for this submission.
6069
`
6170
}
6271

@@ -68,6 +77,7 @@ export function throwPostmanStepError({
6877
blacklistedRecipients,
6978
invalidAttachments,
7079
formAdminLink,
80+
isRetryWithoutAttachments,
7181
}: {
7282
$: IGlobalVariable
7383
status: PostmanEmailSendStatus
@@ -76,6 +86,7 @@ export function throwPostmanStepError({
7686
blacklistedRecipients: string[]
7787
invalidAttachments: string[]
7888
formAdminLink: string | null
89+
isRetryWithoutAttachments: boolean
7990
}) {
8091
const position = $.step.position
8192
const appName = $.app.name
@@ -84,6 +95,8 @@ export function throwPostmanStepError({
8495
const invalidAttachmentsSolution = getInvalidAttachmentSolution({
8596
invalidAttachments,
8697
formAdminLink,
98+
// should not show attachments list if we are retrying without attachments
99+
showAttachmentsList: !isRetryWithoutAttachments,
87100
})
88101

89102
switch (status) {
@@ -106,14 +119,19 @@ export function throwPostmanStepError({
106119
solution += `\n\n \n\n${invalidAttachmentsSolution}`
107120
}
108121

122+
let buttonMessage = 'Resend to blacklisted recipients'
123+
if (isRetryWithoutAttachments) {
124+
buttonMessage += ' without attachments'
125+
}
126+
109127
if (isPartialSuccess) {
110128
throw new PartialStepError({
111129
name,
112130
solution,
113131
position,
114132
appName,
115133
partialRetry: {
116-
buttonMessage: 'Resend to blacklisted recipients',
134+
buttonMessage,
117135
},
118136
})
119137
}
@@ -162,14 +180,9 @@ export function throwPostmanStepError({
162180
// attachments that are password-protected
163181
if (hasInvalidAttachments) {
164182
const name = 'Invalid attachment(s)'
165-
const solution = getInvalidAttachmentSolution({
166-
invalidAttachments,
167-
formAdminLink,
168-
})
169-
170183
throw new PartialStepError({
171184
name,
172-
solution,
185+
solution: invalidAttachmentsSolution,
173186
position,
174187
appName,
175188
partialRetry: { buttonMessage: '' }, // nothing to retry

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)