Improving the email client to support reconnecting

This commit is contained in:
Collin Duncan 2024-09-19 11:49:25 +02:00
parent 4a94d280cb
commit 3b41a75bb4
No known key found for this signature in database
2 changed files with 42 additions and 9 deletions

View file

@ -7,10 +7,14 @@ import { LoggerService } from '../logger/service.logger'
import { NtfyProvider } from '../ntfy/provider' import { NtfyProvider } from '../ntfy/provider'
import { Email } from './types' import { Email } from './types'
const MAX_CONNECTION_ATTEMPTS = 5
const CONNECTION_ATTEMPT_DELAY = [0, 500, 1500, 5000, 1000]
export enum EmailClientStatus { export enum EmailClientStatus {
NotReady, NotReady,
Ready, Ready,
Error, Destroying,
Failed,
} }
@Injectable() @Injectable()
@ -19,6 +23,7 @@ export class EmailClient {
private readonly mailboxName: string private readonly mailboxName: string
private mailbox?: Imap.Box private mailbox?: Imap.Box
private status: EmailClientStatus private status: EmailClientStatus
private connectionAttempts: number
constructor( constructor(
@Inject(LoggerService) @Inject(LoggerService)
@ -30,6 +35,7 @@ export class EmailClient {
@Inject(ConfigService) @Inject(ConfigService)
private readonly configService: ConfigService, private readonly configService: ConfigService,
) { ) {
this.connectionAttempts = 0
this.mailboxName = this.configService.getOrThrow<string>('EMAIL_MAILBOX') this.mailboxName = this.configService.getOrThrow<string>('EMAIL_MAILBOX')
this.imapClient = new Imap({ this.imapClient = new Imap({
host: this.configService.getOrThrow('EMAIL_HOST'), host: this.configService.getOrThrow('EMAIL_HOST'),
@ -44,6 +50,7 @@ export class EmailClient {
} }
onModuleDestroy() { onModuleDestroy() {
this.setStatus(EmailClientStatus.Destroying)
this.imapClient.end() this.imapClient.end()
} }
@ -51,8 +58,23 @@ export class EmailClient {
this.status = status this.status = status
} }
public isConnected() {
return this.status === EmailClientStatus.Ready
}
private connect() { private connect() {
this.imapClient.connect() if (this.connectionAttempts > MAX_CONNECTION_ATTEMPTS) {
this.setStatus(EmailClientStatus.Failed)
return
}
if (this.connectionAttempts === 0) {
this.imapClient.connect()
} else {
const connectionDelay = CONNECTION_ATTEMPT_DELAY[this.connectionAttempts]
setTimeout(this.imapClient.connect, connectionDelay)
}
this.connectionAttempts += 1
} }
private openBox() { private openBox() {
@ -63,7 +85,9 @@ export class EmailClient {
}) })
return return
} }
this.loggerService.debug('Mailbox opened', { mailbox }) this.loggerService.debug('Mailbox opened', {
mailbox: { name: mailbox.name },
})
this.mailbox = mailbox this.mailbox = mailbox
}) })
} }
@ -82,9 +106,11 @@ export class EmailClient {
try { try {
fn() fn()
this.loggerService.debug(`Attempt succeeded`)
} catch (error: unknown) { } catch (error: unknown) {
this.loggerService.debug( this.loggerService.debug(
`Attempting ${label} hit error at attempt ${current}`, `Attempting ${label} hit error at attempt ${current}`,
error,
) )
setTimeout( setTimeout(
() => this.attempt(label, current + 1, max, delayMs, fn), () => this.attempt(label, current + 1, max, delayMs, fn),
@ -219,6 +245,7 @@ export class EmailClient {
private setupDefaultListeners() { private setupDefaultListeners() {
this.imapClient.on('ready', () => { this.imapClient.on('ready', () => {
this.loggerService.debug('email client ready') this.loggerService.debug('email client ready')
this.connectionAttempts = 0
if (this.status === EmailClientStatus.NotReady) { if (this.status === EmailClientStatus.NotReady) {
this.setStatus(EmailClientStatus.Ready) this.setStatus(EmailClientStatus.Ready)
this.openBox() this.openBox()
@ -226,18 +253,20 @@ export class EmailClient {
}) })
this.imapClient.on('close', () => { this.imapClient.on('close', () => {
this.loggerService.debug('email client close') if (this.status === EmailClientStatus.Destroying) {
if (this.status !== EmailClientStatus.Error) { this.loggerService.debug('email client close due to termination')
this.loggerService.debug('email client reconnecting') return
this.setStatus(EmailClientStatus.NotReady)
this.connect()
} }
this.loggerService.debug('email client reconnecting')
this.setStatus(EmailClientStatus.NotReady)
this.connect()
}) })
this.imapClient.on('error', async (error: Error) => { this.imapClient.on('error', async (error: Error) => {
this.loggerService.error(`Error with imap client ${error.message}`) this.loggerService.error(`Error with imap client ${error.message}`)
await this.ntfyProvider.sendEmailClientErrorNotification(error.message) await this.ntfyProvider.sendEmailClientErrorNotification(error.message)
this.setStatus(EmailClientStatus.Error) this.setStatus(EmailClientStatus.NotReady)
}) })
} }

View file

@ -18,6 +18,10 @@ export class EmailProvider {
this.registerEmailListener() this.registerEmailListener()
} }
public isConnected() {
return this.emailClient.isConnected()
}
private async handleReceivedEmails(emails: Email[]) { private async handleReceivedEmails(emails: Email[]) {
await this.emailsQueue.addBulk(emails.map((email) => ({ data: email }))) await this.emailsQueue.addBulk(emails.map((email) => ({ data: email })))
} }