Skip to content

Commit 133463e

Browse files
committed
chore: retry emails without attachments
1 parent 7ecee5f commit 133463e

File tree

4 files changed

+129
-34
lines changed

4 files changed

+129
-34
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
@@ -76,9 +76,6 @@ const action: IRawAction = {
7676
)
7777
}
7878

79-
const { attachmentFiles, invalidAttachments, submissionId, formId } =
80-
await filterAttachments(result.data.attachments, $)
81-
8279
let recipientsToSend = result.data.destinationEmail
8380
/**
8481
* Logic to handle retries here:
@@ -101,6 +98,19 @@ const action: IRawAction = {
10198
// Don't do partial retry in test runs! always send to all recipients
10299
!$.execution.testRun
103100

101+
const {
102+
attachmentFiles,
103+
invalidAttachments,
104+
submissionId,
105+
formId,
106+
isRetryWithoutAttachments,
107+
} = await filterAttachments({
108+
$,
109+
attachmentsList: result.data.attachments,
110+
isPartialRetry,
111+
lastExecutionStep,
112+
})
113+
104114
if (isPartialRetry) {
105115
const { status, recipient } = prevDataOutParseResult.data
106116
recipientsToSend = recipient.filter((_, i) => status[i] !== 'ACCEPTED')
@@ -202,8 +212,14 @@ const action: IRawAction = {
202212
/**
203213
* Send invalid attachments notification email
204214
* Do not send on partial retry as we would have already sent this once with the blacklist email
215+
* Do not send on retry when removing all attachments
205216
*/
206-
if (hasInvalidAttachments && !isPartialRetry && !$.execution.testRun) {
217+
if (
218+
hasInvalidAttachments &&
219+
!isPartialRetry &&
220+
!$.execution.testRun &&
221+
!isRetryWithoutAttachments
222+
) {
207223
await sendInvalidAttachmentsEmail({
208224
...defaultSendEmailParams,
209225
...invalidAttachmentParams,
@@ -229,6 +245,7 @@ const action: IRawAction = {
229245
blacklistedRecipients,
230246
invalidAttachments,
231247
formAdminLink,
248+
isRetryWithoutAttachments,
232249
})
233250
}
234251
},

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'
@@ -25,28 +25,66 @@ export async function getFormId(executionId: string): Promise<string | null> {
2525
return formId ? String(formId) : null
2626
}
2727

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

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

47-
if (formId === null) {
48-
formId = await getFormId($.execution.id)
49-
}
86+
if (!fileType || !POSTMAN_ACCEPTED_EXTENSIONS.includes(fileType)) {
87+
invalidAttachments.push(fileName)
5088

5189
if (submissionId === null) {
5290
submissionId = attachment.slice(
@@ -60,5 +98,22 @@ export async function filterAttachments(
6098
return
6199
}),
62100
)
63-
return { attachmentFiles, invalidAttachments, submissionId, formId }
101+
102+
if (isRetryWithoutAttachments) {
103+
return {
104+
attachmentFiles: [],
105+
invalidAttachments,
106+
submissionId,
107+
formId,
108+
isRetryWithoutAttachments,
109+
}
110+
}
111+
112+
return {
113+
attachmentFiles,
114+
invalidAttachments,
115+
submissionId,
116+
formId,
117+
isRetryWithoutAttachments,
118+
}
64119
}

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&nbsp;\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)