Enhance Inbox model and related services for improved error handling and resilience
Add a new field `consecutiveFailures` to the Inbox model to track consecutive errors. Update the inbox guard logic to mark mailboxes as error after exceeding a failure threshold. Implement retry logic in email and IMAP services to handle transient connection issues. Adjust job processing to reclaim stale send jobs and improve overall reliability of email dispatching. Update routes to initialize `consecutiveFailures` on inbox creation and reset it when the inbox status changes to active.
This commit is contained in:
parent
7f47ec4809
commit
b300293a72
18 changed files with 515 additions and 109 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1247,6 +1247,7 @@ export const InboxScalarFieldEnum = {
|
||||||
companyName: 'companyName',
|
companyName: 'companyName',
|
||||||
lastError: 'lastError',
|
lastError: 'lastError',
|
||||||
errorAt: 'errorAt',
|
errorAt: 'errorAt',
|
||||||
|
consecutiveFailures: 'consecutiveFailures',
|
||||||
spamFolderPath: 'spamFolderPath',
|
spamFolderPath: 'spamFolderPath',
|
||||||
lastSpamRescuedAt: 'lastSpamRescuedAt',
|
lastSpamRescuedAt: 'lastSpamRescuedAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ export const InboxScalarFieldEnum = {
|
||||||
companyName: 'companyName',
|
companyName: 'companyName',
|
||||||
lastError: 'lastError',
|
lastError: 'lastError',
|
||||||
errorAt: 'errorAt',
|
errorAt: 'errorAt',
|
||||||
|
consecutiveFailures: 'consecutiveFailures',
|
||||||
spamFolderPath: 'spamFolderPath',
|
spamFolderPath: 'spamFolderPath',
|
||||||
lastSpamRescuedAt: 'lastSpamRescuedAt',
|
lastSpamRescuedAt: 'lastSpamRescuedAt',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export type InboxAvgAggregateOutputType = {
|
||||||
imapPort: number | null
|
imapPort: number | null
|
||||||
dailyLimit: number | null
|
dailyLimit: number | null
|
||||||
currentRamp: number | null
|
currentRamp: number | null
|
||||||
|
consecutiveFailures: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InboxSumAggregateOutputType = {
|
export type InboxSumAggregateOutputType = {
|
||||||
|
|
@ -38,6 +39,7 @@ export type InboxSumAggregateOutputType = {
|
||||||
imapPort: number | null
|
imapPort: number | null
|
||||||
dailyLimit: number | null
|
dailyLimit: number | null
|
||||||
currentRamp: number | null
|
currentRamp: number | null
|
||||||
|
consecutiveFailures: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InboxMinAggregateOutputType = {
|
export type InboxMinAggregateOutputType = {
|
||||||
|
|
@ -62,6 +64,7 @@ export type InboxMinAggregateOutputType = {
|
||||||
companyName: string | null
|
companyName: string | null
|
||||||
lastError: string | null
|
lastError: string | null
|
||||||
errorAt: Date | null
|
errorAt: Date | null
|
||||||
|
consecutiveFailures: number | null
|
||||||
spamFolderPath: string | null
|
spamFolderPath: string | null
|
||||||
lastSpamRescuedAt: Date | null
|
lastSpamRescuedAt: Date | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
|
|
@ -90,6 +93,7 @@ export type InboxMaxAggregateOutputType = {
|
||||||
companyName: string | null
|
companyName: string | null
|
||||||
lastError: string | null
|
lastError: string | null
|
||||||
errorAt: Date | null
|
errorAt: Date | null
|
||||||
|
consecutiveFailures: number | null
|
||||||
spamFolderPath: string | null
|
spamFolderPath: string | null
|
||||||
lastSpamRescuedAt: Date | null
|
lastSpamRescuedAt: Date | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
|
|
@ -118,6 +122,7 @@ export type InboxCountAggregateOutputType = {
|
||||||
companyName: number
|
companyName: number
|
||||||
lastError: number
|
lastError: number
|
||||||
errorAt: number
|
errorAt: number
|
||||||
|
consecutiveFailures: number
|
||||||
spamFolderPath: number
|
spamFolderPath: number
|
||||||
lastSpamRescuedAt: number
|
lastSpamRescuedAt: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
|
|
@ -131,6 +136,7 @@ export type InboxAvgAggregateInputType = {
|
||||||
imapPort?: true
|
imapPort?: true
|
||||||
dailyLimit?: true
|
dailyLimit?: true
|
||||||
currentRamp?: true
|
currentRamp?: true
|
||||||
|
consecutiveFailures?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InboxSumAggregateInputType = {
|
export type InboxSumAggregateInputType = {
|
||||||
|
|
@ -138,6 +144,7 @@ export type InboxSumAggregateInputType = {
|
||||||
imapPort?: true
|
imapPort?: true
|
||||||
dailyLimit?: true
|
dailyLimit?: true
|
||||||
currentRamp?: true
|
currentRamp?: true
|
||||||
|
consecutiveFailures?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InboxMinAggregateInputType = {
|
export type InboxMinAggregateInputType = {
|
||||||
|
|
@ -162,6 +169,7 @@ export type InboxMinAggregateInputType = {
|
||||||
companyName?: true
|
companyName?: true
|
||||||
lastError?: true
|
lastError?: true
|
||||||
errorAt?: true
|
errorAt?: true
|
||||||
|
consecutiveFailures?: true
|
||||||
spamFolderPath?: true
|
spamFolderPath?: true
|
||||||
lastSpamRescuedAt?: true
|
lastSpamRescuedAt?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
|
|
@ -190,6 +198,7 @@ export type InboxMaxAggregateInputType = {
|
||||||
companyName?: true
|
companyName?: true
|
||||||
lastError?: true
|
lastError?: true
|
||||||
errorAt?: true
|
errorAt?: true
|
||||||
|
consecutiveFailures?: true
|
||||||
spamFolderPath?: true
|
spamFolderPath?: true
|
||||||
lastSpamRescuedAt?: true
|
lastSpamRescuedAt?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
|
|
@ -218,6 +227,7 @@ export type InboxCountAggregateInputType = {
|
||||||
companyName?: true
|
companyName?: true
|
||||||
lastError?: true
|
lastError?: true
|
||||||
errorAt?: true
|
errorAt?: true
|
||||||
|
consecutiveFailures?: true
|
||||||
spamFolderPath?: true
|
spamFolderPath?: true
|
||||||
lastSpamRescuedAt?: true
|
lastSpamRescuedAt?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
|
|
@ -333,6 +343,7 @@ export type InboxGroupByOutputType = {
|
||||||
companyName: string | null
|
companyName: string | null
|
||||||
lastError: string | null
|
lastError: string | null
|
||||||
errorAt: Date | null
|
errorAt: Date | null
|
||||||
|
consecutiveFailures: number
|
||||||
spamFolderPath: string | null
|
spamFolderPath: string | null
|
||||||
lastSpamRescuedAt: Date | null
|
lastSpamRescuedAt: Date | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
@ -384,6 +395,7 @@ export type InboxWhereInput = {
|
||||||
companyName?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
companyName?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
lastError?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
lastError?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
errorAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
errorAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFilter<"Inbox"> | number
|
||||||
spamFolderPath?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
spamFolderPath?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
lastSpamRescuedAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
lastSpamRescuedAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"Inbox"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Inbox"> | Date | string
|
||||||
|
|
@ -417,6 +429,7 @@ export type InboxOrderByWithRelationInput = {
|
||||||
companyName?: Prisma.SortOrderInput | Prisma.SortOrder
|
companyName?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
lastError?: Prisma.SortOrderInput | Prisma.SortOrder
|
lastError?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
errorAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
errorAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
consecutiveFailures?: Prisma.SortOrder
|
||||||
spamFolderPath?: Prisma.SortOrderInput | Prisma.SortOrder
|
spamFolderPath?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
lastSpamRescuedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
lastSpamRescuedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
|
|
@ -454,6 +467,7 @@ export type InboxWhereUniqueInput = Prisma.AtLeast<{
|
||||||
companyName?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
companyName?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
lastError?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
lastError?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
errorAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
errorAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFilter<"Inbox"> | number
|
||||||
spamFolderPath?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
spamFolderPath?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
lastSpamRescuedAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
lastSpamRescuedAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"Inbox"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Inbox"> | Date | string
|
||||||
|
|
@ -487,6 +501,7 @@ export type InboxOrderByWithAggregationInput = {
|
||||||
companyName?: Prisma.SortOrderInput | Prisma.SortOrder
|
companyName?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
lastError?: Prisma.SortOrderInput | Prisma.SortOrder
|
lastError?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
errorAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
errorAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
|
consecutiveFailures?: Prisma.SortOrder
|
||||||
spamFolderPath?: Prisma.SortOrderInput | Prisma.SortOrder
|
spamFolderPath?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
lastSpamRescuedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
lastSpamRescuedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
|
|
@ -523,6 +538,7 @@ export type InboxScalarWhereWithAggregatesInput = {
|
||||||
companyName?: Prisma.StringNullableWithAggregatesFilter<"Inbox"> | string | null
|
companyName?: Prisma.StringNullableWithAggregatesFilter<"Inbox"> | string | null
|
||||||
lastError?: Prisma.StringNullableWithAggregatesFilter<"Inbox"> | string | null
|
lastError?: Prisma.StringNullableWithAggregatesFilter<"Inbox"> | string | null
|
||||||
errorAt?: Prisma.DateTimeNullableWithAggregatesFilter<"Inbox"> | Date | string | null
|
errorAt?: Prisma.DateTimeNullableWithAggregatesFilter<"Inbox"> | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntWithAggregatesFilter<"Inbox"> | number
|
||||||
spamFolderPath?: Prisma.StringNullableWithAggregatesFilter<"Inbox"> | string | null
|
spamFolderPath?: Prisma.StringNullableWithAggregatesFilter<"Inbox"> | string | null
|
||||||
lastSpamRescuedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"Inbox"> | Date | string | null
|
lastSpamRescuedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"Inbox"> | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Inbox"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Inbox"> | Date | string
|
||||||
|
|
@ -550,6 +566,7 @@ export type InboxCreateInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -583,6 +600,7 @@ export type InboxUncheckedCreateInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -614,6 +632,7 @@ export type InboxUpdateInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -647,6 +666,7 @@ export type InboxUncheckedUpdateInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -679,6 +699,7 @@ export type InboxCreateManyInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -706,6 +727,7 @@ export type InboxUpdateManyMutationInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -734,6 +756,7 @@ export type InboxUncheckedUpdateManyInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -777,6 +800,7 @@ export type InboxCountOrderByAggregateInput = {
|
||||||
companyName?: Prisma.SortOrder
|
companyName?: Prisma.SortOrder
|
||||||
lastError?: Prisma.SortOrder
|
lastError?: Prisma.SortOrder
|
||||||
errorAt?: Prisma.SortOrder
|
errorAt?: Prisma.SortOrder
|
||||||
|
consecutiveFailures?: Prisma.SortOrder
|
||||||
spamFolderPath?: Prisma.SortOrder
|
spamFolderPath?: Prisma.SortOrder
|
||||||
lastSpamRescuedAt?: Prisma.SortOrder
|
lastSpamRescuedAt?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
|
|
@ -788,6 +812,7 @@ export type InboxAvgOrderByAggregateInput = {
|
||||||
imapPort?: Prisma.SortOrder
|
imapPort?: Prisma.SortOrder
|
||||||
dailyLimit?: Prisma.SortOrder
|
dailyLimit?: Prisma.SortOrder
|
||||||
currentRamp?: Prisma.SortOrder
|
currentRamp?: Prisma.SortOrder
|
||||||
|
consecutiveFailures?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InboxMaxOrderByAggregateInput = {
|
export type InboxMaxOrderByAggregateInput = {
|
||||||
|
|
@ -812,6 +837,7 @@ export type InboxMaxOrderByAggregateInput = {
|
||||||
companyName?: Prisma.SortOrder
|
companyName?: Prisma.SortOrder
|
||||||
lastError?: Prisma.SortOrder
|
lastError?: Prisma.SortOrder
|
||||||
errorAt?: Prisma.SortOrder
|
errorAt?: Prisma.SortOrder
|
||||||
|
consecutiveFailures?: Prisma.SortOrder
|
||||||
spamFolderPath?: Prisma.SortOrder
|
spamFolderPath?: Prisma.SortOrder
|
||||||
lastSpamRescuedAt?: Prisma.SortOrder
|
lastSpamRescuedAt?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
|
|
@ -840,6 +866,7 @@ export type InboxMinOrderByAggregateInput = {
|
||||||
companyName?: Prisma.SortOrder
|
companyName?: Prisma.SortOrder
|
||||||
lastError?: Prisma.SortOrder
|
lastError?: Prisma.SortOrder
|
||||||
errorAt?: Prisma.SortOrder
|
errorAt?: Prisma.SortOrder
|
||||||
|
consecutiveFailures?: Prisma.SortOrder
|
||||||
spamFolderPath?: Prisma.SortOrder
|
spamFolderPath?: Prisma.SortOrder
|
||||||
lastSpamRescuedAt?: Prisma.SortOrder
|
lastSpamRescuedAt?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
|
|
@ -851,6 +878,7 @@ export type InboxSumOrderByAggregateInput = {
|
||||||
imapPort?: Prisma.SortOrder
|
imapPort?: Prisma.SortOrder
|
||||||
dailyLimit?: Prisma.SortOrder
|
dailyLimit?: Prisma.SortOrder
|
||||||
currentRamp?: Prisma.SortOrder
|
currentRamp?: Prisma.SortOrder
|
||||||
|
consecutiveFailures?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InboxScalarRelationFilter = {
|
export type InboxScalarRelationFilter = {
|
||||||
|
|
@ -1001,6 +1029,7 @@ export type InboxCreateWithoutWorkspaceInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1032,6 +1061,7 @@ export type InboxUncheckedCreateWithoutWorkspaceInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1092,6 +1122,7 @@ export type InboxScalarWhereInput = {
|
||||||
companyName?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
companyName?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
lastError?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
lastError?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
errorAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
errorAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFilter<"Inbox"> | number
|
||||||
spamFolderPath?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
spamFolderPath?: Prisma.StringNullableFilter<"Inbox"> | string | null
|
||||||
lastSpamRescuedAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
lastSpamRescuedAt?: Prisma.DateTimeNullableFilter<"Inbox"> | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFilter<"Inbox"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Inbox"> | Date | string
|
||||||
|
|
@ -1119,6 +1150,7 @@ export type InboxCreateWithoutPrimaryCampaignsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1151,6 +1183,7 @@ export type InboxUncheckedCreateWithoutPrimaryCampaignsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1197,6 +1230,7 @@ export type InboxUpdateWithoutPrimaryCampaignsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1229,6 +1263,7 @@ export type InboxUncheckedUpdateWithoutPrimaryCampaignsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1259,6 +1294,7 @@ export type InboxCreateWithoutSatelliteCampaignsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1291,6 +1327,7 @@ export type InboxUncheckedCreateWithoutSatelliteCampaignsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1337,6 +1374,7 @@ export type InboxUpdateWithoutSatelliteCampaignsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1369,6 +1407,7 @@ export type InboxUncheckedUpdateWithoutSatelliteCampaignsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1399,6 +1438,7 @@ export type InboxCreateWithoutSentLogsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1431,6 +1471,7 @@ export type InboxUncheckedCreateWithoutSentLogsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1466,6 +1507,7 @@ export type InboxCreateWithoutReceivedLogsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1498,6 +1540,7 @@ export type InboxUncheckedCreateWithoutReceivedLogsInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1544,6 +1587,7 @@ export type InboxUpdateWithoutSentLogsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1576,6 +1620,7 @@ export type InboxUncheckedUpdateWithoutSentLogsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1617,6 +1662,7 @@ export type InboxUpdateWithoutReceivedLogsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1649,6 +1695,7 @@ export type InboxUncheckedUpdateWithoutReceivedLogsInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1679,6 +1726,7 @@ export type InboxCreateManyWorkspaceInput = {
|
||||||
companyName?: string | null
|
companyName?: string | null
|
||||||
lastError?: string | null
|
lastError?: string | null
|
||||||
errorAt?: Date | string | null
|
errorAt?: Date | string | null
|
||||||
|
consecutiveFailures?: number
|
||||||
spamFolderPath?: string | null
|
spamFolderPath?: string | null
|
||||||
lastSpamRescuedAt?: Date | string | null
|
lastSpamRescuedAt?: Date | string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
|
@ -1706,6 +1754,7 @@ export type InboxUpdateWithoutWorkspaceInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1737,6 +1786,7 @@ export type InboxUncheckedUpdateWithoutWorkspaceInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1768,6 +1818,7 @@ export type InboxUncheckedUpdateManyWithoutWorkspaceInput = {
|
||||||
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
companyName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
lastError?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
errorAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
consecutiveFailures?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
spamFolderPath?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
lastSpamRescuedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
|
@ -1854,6 +1905,7 @@ export type InboxSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||||
companyName?: boolean
|
companyName?: boolean
|
||||||
lastError?: boolean
|
lastError?: boolean
|
||||||
errorAt?: boolean
|
errorAt?: boolean
|
||||||
|
consecutiveFailures?: boolean
|
||||||
spamFolderPath?: boolean
|
spamFolderPath?: boolean
|
||||||
lastSpamRescuedAt?: boolean
|
lastSpamRescuedAt?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
|
|
@ -1888,6 +1940,7 @@ export type InboxSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensi
|
||||||
companyName?: boolean
|
companyName?: boolean
|
||||||
lastError?: boolean
|
lastError?: boolean
|
||||||
errorAt?: boolean
|
errorAt?: boolean
|
||||||
|
consecutiveFailures?: boolean
|
||||||
spamFolderPath?: boolean
|
spamFolderPath?: boolean
|
||||||
lastSpamRescuedAt?: boolean
|
lastSpamRescuedAt?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
|
|
@ -1917,6 +1970,7 @@ export type InboxSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
|
||||||
companyName?: boolean
|
companyName?: boolean
|
||||||
lastError?: boolean
|
lastError?: boolean
|
||||||
errorAt?: boolean
|
errorAt?: boolean
|
||||||
|
consecutiveFailures?: boolean
|
||||||
spamFolderPath?: boolean
|
spamFolderPath?: boolean
|
||||||
lastSpamRescuedAt?: boolean
|
lastSpamRescuedAt?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
|
|
@ -1946,13 +2000,14 @@ export type InboxSelectScalar = {
|
||||||
companyName?: boolean
|
companyName?: boolean
|
||||||
lastError?: boolean
|
lastError?: boolean
|
||||||
errorAt?: boolean
|
errorAt?: boolean
|
||||||
|
consecutiveFailures?: boolean
|
||||||
spamFolderPath?: boolean
|
spamFolderPath?: boolean
|
||||||
lastSpamRescuedAt?: boolean
|
lastSpamRescuedAt?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InboxOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "workspaceId" | "email" | "smtpHost" | "smtpPort" | "imapHost" | "imapPort" | "username" | "password" | "authType" | "oauthRefreshToken" | "role" | "provider" | "status" | "dailyLimit" | "currentRamp" | "industry" | "senderName" | "companyName" | "lastError" | "errorAt" | "spamFolderPath" | "lastSpamRescuedAt" | "createdAt" | "updatedAt", ExtArgs["result"]["inbox"]>
|
export type InboxOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "workspaceId" | "email" | "smtpHost" | "smtpPort" | "imapHost" | "imapPort" | "username" | "password" | "authType" | "oauthRefreshToken" | "role" | "provider" | "status" | "dailyLimit" | "currentRamp" | "industry" | "senderName" | "companyName" | "lastError" | "errorAt" | "consecutiveFailures" | "spamFolderPath" | "lastSpamRescuedAt" | "createdAt" | "updatedAt", ExtArgs["result"]["inbox"]>
|
||||||
export type InboxInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type InboxInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
workspace?: boolean | Prisma.WorkspaceDefaultArgs<ExtArgs>
|
workspace?: boolean | Prisma.WorkspaceDefaultArgs<ExtArgs>
|
||||||
sentLogs?: boolean | Prisma.Inbox$sentLogsArgs<ExtArgs>
|
sentLogs?: boolean | Prisma.Inbox$sentLogsArgs<ExtArgs>
|
||||||
|
|
@ -1999,6 +2054,7 @@ export type $InboxPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
||||||
companyName: string | null
|
companyName: string | null
|
||||||
lastError: string | null
|
lastError: string | null
|
||||||
errorAt: Date | null
|
errorAt: Date | null
|
||||||
|
consecutiveFailures: number
|
||||||
spamFolderPath: string | null
|
spamFolderPath: string | null
|
||||||
lastSpamRescuedAt: Date | null
|
lastSpamRescuedAt: Date | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
@ -2452,6 +2508,7 @@ export interface InboxFieldRefs {
|
||||||
readonly companyName: Prisma.FieldRef<"Inbox", 'String'>
|
readonly companyName: Prisma.FieldRef<"Inbox", 'String'>
|
||||||
readonly lastError: Prisma.FieldRef<"Inbox", 'String'>
|
readonly lastError: Prisma.FieldRef<"Inbox", 'String'>
|
||||||
readonly errorAt: Prisma.FieldRef<"Inbox", 'DateTime'>
|
readonly errorAt: Prisma.FieldRef<"Inbox", 'DateTime'>
|
||||||
|
readonly consecutiveFailures: Prisma.FieldRef<"Inbox", 'Int'>
|
||||||
readonly spamFolderPath: Prisma.FieldRef<"Inbox", 'String'>
|
readonly spamFolderPath: Prisma.FieldRef<"Inbox", 'String'>
|
||||||
readonly lastSpamRescuedAt: Prisma.FieldRef<"Inbox", 'DateTime'>
|
readonly lastSpamRescuedAt: Prisma.FieldRef<"Inbox", 'DateTime'>
|
||||||
readonly createdAt: Prisma.FieldRef<"Inbox", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"Inbox", 'DateTime'>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
claimDueSendJob,
|
claimDueSendJob,
|
||||||
completeSendJob,
|
completeSendJob,
|
||||||
enqueueSendJob,
|
enqueueSendJob,
|
||||||
|
reclaimStaleSendJobs,
|
||||||
} from '../services/job-queue.service';
|
} from '../services/job-queue.service';
|
||||||
|
|
||||||
const MIN_JITTER_MS = 45 * 60 * 1000;
|
const MIN_JITTER_MS = 45 * 60 * 1000;
|
||||||
|
|
@ -79,12 +80,19 @@ async function executeCampaignSend(
|
||||||
const satellite = pickWeightedSatellite(activeSatellites);
|
const satellite = pickWeightedSatellite(activeSatellites);
|
||||||
const primary = campaign.primaryMailbox;
|
const primary = campaign.primaryMailbox;
|
||||||
|
|
||||||
if (primary.status !== InboxStatus.active) {
|
if (primary.status === InboxStatus.paused) {
|
||||||
logger.warn({ campaignId }, 'Primary mailbox is not active');
|
logger.warn({ campaignId }, 'Primary mailbox is paused — skipping send');
|
||||||
if (jobId) await completeSendJob(jobId, false, 'Primary mailbox inactive');
|
if (jobId) await completeSendJob(jobId, false, 'Primary mailbox paused');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (primary.status === InboxStatus.error) {
|
||||||
|
logger.warn(
|
||||||
|
{ campaignId, primary: primary.email },
|
||||||
|
'Primary mailbox in error — proceeding with satellite send (IMAP not required)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAllowedWarmupSendPair(satellite, primary)) {
|
if (!isAllowedWarmupSendPair(satellite, primary)) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ campaignId, satellite: satellite.email, primary: primary.email },
|
{ campaignId, satellite: satellite.email, primary: primary.email },
|
||||||
|
|
@ -94,13 +102,14 @@ async function executeCampaignSend(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await withInboxGuard(satellite, async () => {
|
try {
|
||||||
const { subject, body, validationResult } =
|
const { subject, body, validationResult } =
|
||||||
await aiService.generateInitialEmailWithValidation(primary.industry, {
|
await aiService.generateInitialEmailWithValidation(primary.industry, {
|
||||||
senderName: satellite.senderName,
|
senderName: satellite.senderName,
|
||||||
companyName: satellite.companyName,
|
companyName: satellite.companyName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await withInboxGuard(satellite, async () => {
|
||||||
const { messageId } = await emailService.sendMail(satellite, {
|
const { messageId } = await emailService.sendMail(satellite, {
|
||||||
to: primary.email,
|
to: primary.email,
|
||||||
subject,
|
subject,
|
||||||
|
|
@ -139,8 +148,6 @@ async function executeCampaignSend(
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (jobId) await completeSendJob(jobId, true);
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
campaignId: campaign.id,
|
campaignId: campaign.id,
|
||||||
|
|
@ -151,11 +158,20 @@ async function executeCampaignSend(
|
||||||
'Campaign warmup email dispatched',
|
'Campaign warmup email dispatched',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (jobId) await completeSendJob(jobId, true);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Campaign send failed';
|
||||||
|
if (jobId) await completeSendJob(jobId, false, message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runCampaignDispatcherJob(): Promise<string> {
|
export async function runCampaignDispatcherJob(): Promise<string> {
|
||||||
logger.info('Campaign dispatcher job started');
|
logger.info('Campaign dispatcher job started');
|
||||||
|
|
||||||
|
await reclaimStaleSendJobs();
|
||||||
|
|
||||||
const campaigns = await prisma.warmupCampaign.findMany({
|
const campaigns = await prisma.warmupCampaign.findMany({
|
||||||
where: {
|
where: {
|
||||||
status: { in: [CampaignStatus.active, CampaignStatus.maintenance] },
|
status: { in: [CampaignStatus.active, CampaignStatus.maintenance] },
|
||||||
|
|
|
||||||
|
|
@ -93,11 +93,12 @@ export async function runDispatcherJob(): Promise<void> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await withInboxGuard(sender, async () => {
|
|
||||||
const { subject, body } = await aiService.generateInitialEmail(sender.industry, {
|
const { subject, body } = await aiService.generateInitialEmail(sender.industry, {
|
||||||
senderName: sender.senderName,
|
senderName: sender.senderName,
|
||||||
companyName: sender.companyName,
|
companyName: sender.companyName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await withInboxGuard(sender, async () => {
|
||||||
const { messageId } = await emailService.sendMail(sender, {
|
const { messageId } = await emailService.sendMail(sender, {
|
||||||
to: receiver.email,
|
to: receiver.email,
|
||||||
subject,
|
subject,
|
||||||
|
|
|
||||||
|
|
@ -458,7 +458,7 @@ export async function runRescuerJob(): Promise<void> {
|
||||||
|
|
||||||
for (const primaryMailboxId of primaryIds) {
|
for (const primaryMailboxId of primaryIds) {
|
||||||
const inbox = await prisma.inbox.findUnique({ where: { id: primaryMailboxId } });
|
const inbox = await prisma.inbox.findUnique({ where: { id: primaryMailboxId } });
|
||||||
if (!inbox || inbox.status !== InboxStatus.active) continue;
|
if (!inbox || inbox.status === InboxStatus.paused) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await processCampaignPrimary(inbox);
|
await processCampaignPrimary(inbox);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import type { Inbox } from '../lib/prisma';
|
import type { Inbox } from '../lib/prisma';
|
||||||
import { InboxStatus } from '../lib/prisma';
|
import { InboxStatus } from '../lib/prisma';
|
||||||
import { prisma } from '../lib/prisma';
|
import { prisma } from '../lib/prisma';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
/** Consecutive failed operations before marking a mailbox as error. */
|
||||||
|
export const MAILBOX_FAILURE_THRESHOLD = 3;
|
||||||
|
|
||||||
export async function withInboxGuard<T>(
|
export async function withInboxGuard<T>(
|
||||||
inbox: Inbox,
|
inbox: Inbox,
|
||||||
|
|
@ -9,24 +13,49 @@ export async function withInboxGuard<T>(
|
||||||
try {
|
try {
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
|
|
||||||
if (inbox.status === InboxStatus.error) {
|
|
||||||
await prisma.inbox.update({
|
await prisma.inbox.update({
|
||||||
where: { id: inbox.id },
|
where: { id: inbox.id },
|
||||||
data: { status: InboxStatus.active, lastError: null, errorAt: null },
|
data: {
|
||||||
});
|
consecutiveFailures: 0,
|
||||||
|
...(inbox.status === InboxStatus.error
|
||||||
|
? {
|
||||||
|
status: InboxStatus.active,
|
||||||
|
lastError: null,
|
||||||
|
errorAt: null,
|
||||||
}
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
await prisma.inbox.update({
|
const updated = await prisma.inbox.update({
|
||||||
where: { id: inbox.id },
|
where: { id: inbox.id },
|
||||||
data: {
|
data: {
|
||||||
status: InboxStatus.error,
|
consecutiveFailures: { increment: 1 },
|
||||||
lastError: message,
|
lastError: message,
|
||||||
errorAt: new Date(),
|
errorAt: new Date(),
|
||||||
},
|
},
|
||||||
|
select: { consecutiveFailures: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (updated.consecutiveFailures >= MAILBOX_FAILURE_THRESHOLD) {
|
||||||
|
await prisma.inbox.update({
|
||||||
|
where: { id: inbox.id },
|
||||||
|
data: { status: InboxStatus.error },
|
||||||
|
});
|
||||||
|
logger.error(
|
||||||
|
{ inboxId: inbox.id, email: inbox.email, consecutiveFailures: updated.consecutiveFailures, message },
|
||||||
|
'Mailbox marked error after consecutive failures',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
{ inboxId: inbox.id, email: inbox.email, consecutiveFailures: updated.consecutiveFailures, message },
|
||||||
|
'Mailbox operation failed (still active)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
apps/api/src/lib/retry.test.ts
Normal file
53
apps/api/src/lib/retry.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import { isRetryableConnectionError, withRetry } from './retry';
|
||||||
|
|
||||||
|
describe('retry', () => {
|
||||||
|
it('treats timeouts as retryable', () => {
|
||||||
|
assert.equal(isRetryableConnectionError(new Error('Connection timeout')), true);
|
||||||
|
assert.equal(isRetryableConnectionError(new Error('ETIMEDOUT')), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats auth failures as non-retryable', () => {
|
||||||
|
assert.equal(isRetryableConnectionError(new Error('Invalid credentials')), false);
|
||||||
|
assert.equal(isRetryableConnectionError(new Error('Authentication failed')), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns on first success', async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const result = await withRetry(async () => {
|
||||||
|
calls += 1;
|
||||||
|
return 'ok';
|
||||||
|
});
|
||||||
|
assert.equal(result, 'ok');
|
||||||
|
assert.equal(calls, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries transient errors then succeeds', async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
|
calls += 1;
|
||||||
|
if (calls === 1) {
|
||||||
|
throw new Error('ETIMEDOUT');
|
||||||
|
}
|
||||||
|
return 'ok';
|
||||||
|
},
|
||||||
|
{ baseDelayMs: 1 },
|
||||||
|
);
|
||||||
|
assert.equal(result, 'ok');
|
||||||
|
assert.equal(calls, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not retry auth errors', async () => {
|
||||||
|
let calls = 0;
|
||||||
|
await assert.rejects(
|
||||||
|
withRetry(async () => {
|
||||||
|
calls += 1;
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}),
|
||||||
|
/Invalid credentials/,
|
||||||
|
);
|
||||||
|
assert.equal(calls, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
98
apps/api/src/lib/retry.ts
Normal file
98
apps/api/src/lib/retry.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
const DEFAULT_MAX_ATTEMPTS = 3;
|
||||||
|
const DEFAULT_BASE_DELAY_MS = 1_000;
|
||||||
|
|
||||||
|
const NON_RETRYABLE_PATTERNS = [
|
||||||
|
'invalid credentials',
|
||||||
|
'authentication failed',
|
||||||
|
'authenticationfailed',
|
||||||
|
'invalid login',
|
||||||
|
'username and password not accepted',
|
||||||
|
'oauth',
|
||||||
|
'not configured',
|
||||||
|
'validation failed',
|
||||||
|
'content validation',
|
||||||
|
];
|
||||||
|
|
||||||
|
const RETRYABLE_PATTERNS = [
|
||||||
|
'timeout',
|
||||||
|
'etimedout',
|
||||||
|
'econnreset',
|
||||||
|
'econnrefused',
|
||||||
|
'enotfound',
|
||||||
|
'socket',
|
||||||
|
'connection lost',
|
||||||
|
'connection closed',
|
||||||
|
'too many',
|
||||||
|
'rate limit',
|
||||||
|
'temporarily',
|
||||||
|
'try again',
|
||||||
|
'service unavailable',
|
||||||
|
'unavailable',
|
||||||
|
'503',
|
||||||
|
'421',
|
||||||
|
'454',
|
||||||
|
'4.7.0',
|
||||||
|
'system error',
|
||||||
|
'network',
|
||||||
|
'closed',
|
||||||
|
'broken pipe',
|
||||||
|
'eai_again',
|
||||||
|
];
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRetryableConnectionError(err: unknown): boolean {
|
||||||
|
const message = errorMessage(err).toLowerCase();
|
||||||
|
|
||||||
|
if (NON_RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withRetry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
options?: {
|
||||||
|
maxAttempts?: number;
|
||||||
|
baseDelayMs?: number;
|
||||||
|
label?: string;
|
||||||
|
},
|
||||||
|
): Promise<T> {
|
||||||
|
const maxAttempts = options?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
||||||
|
const baseDelayMs = options?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
||||||
|
const label = options?.label ?? 'operation';
|
||||||
|
|
||||||
|
let lastError: unknown;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
const retryable = isRetryableConnectionError(err);
|
||||||
|
const isLastAttempt = attempt === maxAttempts;
|
||||||
|
|
||||||
|
if (!retryable || isLastAttempt) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayMs = baseDelayMs * 2 ** (attempt - 1);
|
||||||
|
logger.warn(
|
||||||
|
{ attempt, maxAttempts, delayMs, label, err: errorMessage(err) },
|
||||||
|
'Retrying after transient connection failure',
|
||||||
|
);
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ export const DEFAULT_INDUSTRIES = ['SaaS', 'Marketing', 'Finance', 'Childcare']
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS = {
|
export const DEFAULT_SETTINGS = {
|
||||||
dispatcher_cron: '*/15 * * * *',
|
dispatcher_cron: '*/15 * * * *',
|
||||||
rescuer_cron: '*/10 * * * *',
|
rescuer_cron: '*/20 * * * *',
|
||||||
ramp_increment: '2',
|
ramp_increment: '2',
|
||||||
industries: JSON.stringify(DEFAULT_INDUSTRIES),
|
industries: JSON.stringify(DEFAULT_INDUSTRIES),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ router.post('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||||
companyName: data.companyName ?? null,
|
companyName: data.companyName ?? null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
errorAt: null,
|
errorAt: null,
|
||||||
|
consecutiveFailures: 0,
|
||||||
spamFolderPath: null,
|
spamFolderPath: null,
|
||||||
lastSpamRescuedAt: null,
|
lastSpamRescuedAt: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
|
@ -300,7 +301,7 @@ router.patch('/:id', async (req: Request, res: Response, next: NextFunction) =>
|
||||||
...(password ? { password: encrypt(password) } : {}),
|
...(password ? { password: encrypt(password) } : {}),
|
||||||
...(oauthRefreshToken ? { oauthRefreshToken: encrypt(oauthRefreshToken) } : {}),
|
...(oauthRefreshToken ? { oauthRefreshToken: encrypt(oauthRefreshToken) } : {}),
|
||||||
...(rest.status === InboxStatus.active
|
...(rest.status === InboxStatus.active
|
||||||
? { lastError: null, errorAt: null }
|
? { lastError: null, errorAt: null, consecutiveFailures: 0 }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -351,10 +352,15 @@ router.post('/:id/test', async (req: Request, res: Response, next: NextFunction)
|
||||||
errorAt: new Date(),
|
errorAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (inbox.status === InboxStatus.error) {
|
} else {
|
||||||
await prisma.inbox.update({
|
await prisma.inbox.update({
|
||||||
where: { id: inbox.id },
|
where: { id: inbox.id },
|
||||||
data: { status: InboxStatus.active, lastError: null, errorAt: null },
|
data: {
|
||||||
|
status: InboxStatus.active,
|
||||||
|
lastError: null,
|
||||||
|
errorAt: null,
|
||||||
|
consecutiveFailures: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import nodemailer, { Transporter } from 'nodemailer';
|
import nodemailer, { Transporter } from 'nodemailer';
|
||||||
import type { Inbox } from '../lib/prisma';
|
import type { Inbox } from '../lib/prisma';
|
||||||
import { MailboxAuthType } from '../lib/prisma';
|
import { MailboxAuthType } from '../lib/prisma';
|
||||||
|
import { withRetry } from '../lib/retry';
|
||||||
import { decrypt } from './crypto.service';
|
import { decrypt } from './crypto.service';
|
||||||
import { EmailPayload } from '../models/types';
|
import { EmailPayload } from '../models/types';
|
||||||
import { resolveMailboxAccessToken } from './mailbox-auth.service';
|
import { resolveMailboxAccessToken } from './mailbox-auth.service';
|
||||||
|
|
@ -58,9 +59,18 @@ export async function createSmtpTransport(inbox: Inbox): Promise<Transporter> {
|
||||||
export async function testSmtp(
|
export async function testSmtp(
|
||||||
inbox: Inbox,
|
inbox: Inbox,
|
||||||
): Promise<{ ok: boolean; error?: string }> {
|
): Promise<{ ok: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
await withRetry(
|
||||||
|
async () => {
|
||||||
const transporter = await createSmtpTransport(inbox);
|
const transporter = await createSmtpTransport(inbox);
|
||||||
try {
|
try {
|
||||||
await transporter.verify();
|
await transporter.verify();
|
||||||
|
} finally {
|
||||||
|
transporter.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ label: `smtp-test:${inbox.email}` },
|
||||||
|
);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'SMTP verification failed';
|
const message = err instanceof Error ? err.message : 'SMTP verification failed';
|
||||||
|
|
@ -68,8 +78,6 @@ export async function testSmtp(
|
||||||
ok: false,
|
ok: false,
|
||||||
error: formatMicrosoftAuthError(message),
|
error: formatMicrosoftAuthError(message),
|
||||||
};
|
};
|
||||||
} finally {
|
|
||||||
transporter.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,6 +85,8 @@ export async function sendMail(
|
||||||
inbox: Inbox,
|
inbox: Inbox,
|
||||||
payload: EmailPayload,
|
payload: EmailPayload,
|
||||||
): Promise<{ messageId: string }> {
|
): Promise<{ messageId: string }> {
|
||||||
|
return withRetry(
|
||||||
|
async () => {
|
||||||
const transporter = await createSmtpTransport(inbox);
|
const transporter = await createSmtpTransport(inbox);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -94,6 +104,9 @@ export async function sendMail(
|
||||||
} finally {
|
} finally {
|
||||||
transporter.close();
|
transporter.close();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ label: `smtp-send:${inbox.email}` },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function testConnection(
|
export async function testConnection(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ImapFlow } from 'imapflow';
|
import { ImapFlow } from 'imapflow';
|
||||||
import type { Inbox } from '../lib/prisma';
|
import type { Inbox } from '../lib/prisma';
|
||||||
import { MailboxAuthType } from '../lib/prisma';
|
import { MailboxAuthType } from '../lib/prisma';
|
||||||
|
import { withRetry } from '../lib/retry';
|
||||||
import { decrypt } from './crypto.service';
|
import { decrypt } from './crypto.service';
|
||||||
import { resolveMailboxAccessToken } from './mailbox-auth.service';
|
import { resolveMailboxAccessToken } from './mailbox-auth.service';
|
||||||
import { formatMicrosoftAuthError } from './microsoft-oauth.service';
|
import { formatMicrosoftAuthError } from './microsoft-oauth.service';
|
||||||
|
|
@ -50,17 +51,13 @@ export async function createImapClient(inbox: Inbox): Promise<ImapFlow> {
|
||||||
export async function testImap(
|
export async function testImap(
|
||||||
inbox: Inbox,
|
inbox: Inbox,
|
||||||
): Promise<{ ok: boolean; error?: string }> {
|
): Promise<{ ok: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
await withRetry(
|
||||||
|
async () => {
|
||||||
const client = await createImapClient(inbox);
|
const client = await createImapClient(inbox);
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
await client.connect();
|
||||||
await client.mailboxOpen('INBOX');
|
await client.mailboxOpen('INBOX');
|
||||||
return { ok: true };
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : 'IMAP connection failed';
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: formatMicrosoftAuthError(message),
|
|
||||||
};
|
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
await client.logout();
|
await client.logout();
|
||||||
|
|
@ -68,12 +65,25 @@ export async function testImap(
|
||||||
// ignore logout errors
|
// ignore logout errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ label: `imap-test:${inbox.email}` },
|
||||||
|
);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'IMAP connection failed';
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: formatMicrosoftAuthError(message),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function withImapClient<T>(
|
export async function withImapClient<T>(
|
||||||
inbox: Inbox,
|
inbox: Inbox,
|
||||||
fn: (client: ImapFlow) => Promise<T>,
|
fn: (client: ImapFlow) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
return withRetry(
|
||||||
|
async () => {
|
||||||
const client = await createImapClient(inbox);
|
const client = await createImapClient(inbox);
|
||||||
await client.connect();
|
await client.connect();
|
||||||
try {
|
try {
|
||||||
|
|
@ -85,4 +95,7 @@ export async function withImapClient<T>(
|
||||||
// ignore logout errors
|
// ignore logout errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ label: `imap:${inbox.email}` },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,29 @@ export async function enqueueSendJob(
|
||||||
return job.id;
|
return job.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STALE_SEND_JOB_MS = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export async function reclaimStaleSendJobs(staleMs = STALE_SEND_JOB_MS): Promise<number> {
|
||||||
|
const cutoff = new Date(Date.now() - staleMs);
|
||||||
|
const result = await prisma.sendJob.updateMany({
|
||||||
|
where: {
|
||||||
|
status: SendJobStatus.processing,
|
||||||
|
updatedAt: { lt: cutoff },
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: SendJobStatus.failed,
|
||||||
|
errorMessage: 'Stale processing job reclaimed',
|
||||||
|
processedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.count > 0) {
|
||||||
|
logger.warn({ count: result.count }, 'Reclaimed stale send jobs stuck in processing');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.count;
|
||||||
|
}
|
||||||
|
|
||||||
export async function claimDueSendJob(campaignId: string) {
|
export async function claimDueSendJob(campaignId: string) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const job = await prisma.sendJob.findFirst({
|
const job = await prisma.sendJob.findFirst({
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,17 @@ export async function seedDefaultSettings(): Promise<void> {
|
||||||
update: {},
|
update: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await migrateLegacyRescuerCron();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One-time bump for installs still on the old 10-minute rescuer interval. */
|
||||||
|
async function migrateLegacyRescuerCron(): Promise<void> {
|
||||||
|
const row = await prisma.setting.findUnique({ where: { key: 'rescuer_cron' } });
|
||||||
|
if (row?.value === '*/10 * * * *') {
|
||||||
|
await prisma.setting.update({
|
||||||
|
where: { key: 'rescuer_cron' },
|
||||||
|
data: { value: DEFAULT_SETTINGS.rescuer_cron },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Inbox" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"smtpHost" TEXT NOT NULL,
|
||||||
|
"smtpPort" INTEGER NOT NULL,
|
||||||
|
"imapHost" TEXT NOT NULL,
|
||||||
|
"imapPort" INTEGER NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"authType" TEXT NOT NULL DEFAULT 'password',
|
||||||
|
"oauthRefreshToken" TEXT,
|
||||||
|
"role" TEXT NOT NULL DEFAULT 'unassigned',
|
||||||
|
"provider" TEXT NOT NULL DEFAULT 'custom',
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'active',
|
||||||
|
"dailyLimit" INTEGER NOT NULL DEFAULT 40,
|
||||||
|
"currentRamp" INTEGER NOT NULL DEFAULT 5,
|
||||||
|
"industry" TEXT NOT NULL DEFAULT 'SaaS',
|
||||||
|
"senderName" TEXT,
|
||||||
|
"companyName" TEXT,
|
||||||
|
"lastError" TEXT,
|
||||||
|
"errorAt" DATETIME,
|
||||||
|
"consecutiveFailures" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"spamFolderPath" TEXT,
|
||||||
|
"lastSpamRescuedAt" DATETIME,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Inbox_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Inbox" ("authType", "companyName", "createdAt", "currentRamp", "dailyLimit", "email", "errorAt", "id", "imapHost", "imapPort", "industry", "lastError", "lastSpamRescuedAt", "oauthRefreshToken", "password", "provider", "role", "senderName", "smtpHost", "smtpPort", "spamFolderPath", "status", "updatedAt", "username", "workspaceId") SELECT "authType", "companyName", "createdAt", "currentRamp", "dailyLimit", "email", "errorAt", "id", "imapHost", "imapPort", "industry", "lastError", "lastSpamRescuedAt", "oauthRefreshToken", "password", "provider", "role", "senderName", "smtpHost", "smtpPort", "spamFolderPath", "status", "updatedAt", "username", "workspaceId" FROM "Inbox";
|
||||||
|
DROP TABLE "Inbox";
|
||||||
|
ALTER TABLE "new_Inbox" RENAME TO "Inbox";
|
||||||
|
CREATE INDEX "Inbox_status_idx" ON "Inbox"("status");
|
||||||
|
CREATE INDEX "Inbox_workspaceId_role_idx" ON "Inbox"("workspaceId", "role");
|
||||||
|
CREATE UNIQUE INDEX "Inbox_workspaceId_email_key" ON "Inbox"("workspaceId", "email");
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Inbox" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"smtpHost" TEXT NOT NULL,
|
||||||
|
"smtpPort" INTEGER NOT NULL,
|
||||||
|
"imapHost" TEXT NOT NULL,
|
||||||
|
"imapPort" INTEGER NOT NULL,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"authType" TEXT NOT NULL DEFAULT 'password',
|
||||||
|
"oauthRefreshToken" TEXT,
|
||||||
|
"role" TEXT NOT NULL DEFAULT 'unassigned',
|
||||||
|
"provider" TEXT NOT NULL DEFAULT 'custom',
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'active',
|
||||||
|
"dailyLimit" INTEGER NOT NULL DEFAULT 40,
|
||||||
|
"currentRamp" INTEGER NOT NULL DEFAULT 5,
|
||||||
|
"industry" TEXT NOT NULL DEFAULT 'SaaS',
|
||||||
|
"senderName" TEXT,
|
||||||
|
"companyName" TEXT,
|
||||||
|
"lastError" TEXT,
|
||||||
|
"errorAt" DATETIME,
|
||||||
|
"consecutiveFailures" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"spamFolderPath" TEXT,
|
||||||
|
"lastSpamRescuedAt" DATETIME,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Inbox_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Inbox" ("authType", "companyName", "createdAt", "currentRamp", "dailyLimit", "email", "errorAt", "id", "imapHost", "imapPort", "industry", "lastError", "lastSpamRescuedAt", "oauthRefreshToken", "password", "provider", "role", "senderName", "smtpHost", "smtpPort", "spamFolderPath", "status", "updatedAt", "username", "workspaceId") SELECT "authType", "companyName", "createdAt", "currentRamp", "dailyLimit", "email", "errorAt", "id", "imapHost", "imapPort", "industry", "lastError", "lastSpamRescuedAt", "oauthRefreshToken", "password", "provider", "role", "senderName", "smtpHost", "smtpPort", "spamFolderPath", "status", "updatedAt", "username", "workspaceId" FROM "Inbox";
|
||||||
|
DROP TABLE "Inbox";
|
||||||
|
ALTER TABLE "new_Inbox" RENAME TO "Inbox";
|
||||||
|
CREATE INDEX "Inbox_status_idx" ON "Inbox"("status");
|
||||||
|
CREATE INDEX "Inbox_workspaceId_role_idx" ON "Inbox"("workspaceId", "role");
|
||||||
|
CREATE UNIQUE INDEX "Inbox_workspaceId_email_key" ON "Inbox"("workspaceId", "email");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
|
|
||||||
|
-- Reduce IMAP polling frequency for existing installs still on the old default.
|
||||||
|
UPDATE "Setting" SET "value" = '*/20 * * * *' WHERE "key" = 'rescuer_cron' AND "value" = '*/10 * * * *';
|
||||||
|
|
@ -121,6 +121,7 @@ model Inbox {
|
||||||
companyName String?
|
companyName String?
|
||||||
lastError String?
|
lastError String?
|
||||||
errorAt DateTime?
|
errorAt DateTime?
|
||||||
|
consecutiveFailures Int @default(0)
|
||||||
spamFolderPath String?
|
spamFolderPath String?
|
||||||
lastSpamRescuedAt DateTime?
|
lastSpamRescuedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue