diff --git a/apps/papra-server/package.json b/apps/papra-server/package.json index 33cd606..d6d9694 100644 --- a/apps/papra-server/package.json +++ b/apps/papra-server/package.json @@ -37,7 +37,7 @@ "@crowlog/logger": "^1.1.0", "@hono/node-server": "^1.13.7", "@libsql/client": "^0.14.0", - "@owlrelay/api-sdk": "^0.0.1", + "@owlrelay/api-sdk": "^0.0.2", "@owlrelay/webhook": "^0.0.3", "@papra/lecture": "^0.0.4", "@paralleldrive/cuid2": "^2.2.2", diff --git a/apps/papra-server/src/modules/intake-emails/drivers/intake-emails.drivers.models.ts b/apps/papra-server/src/modules/intake-emails/drivers/intake-emails.drivers.models.ts index 85a95e9..40f3e56 100644 --- a/apps/papra-server/src/modules/intake-emails/drivers/intake-emails.drivers.models.ts +++ b/apps/papra-server/src/modules/intake-emails/drivers/intake-emails.drivers.models.ts @@ -3,6 +3,7 @@ import type { Config } from '../../config/config.types'; export type IntakeEmailsServices = { name: string; generateEmailAddress: () => Promise<{ emailAddress: string }>; + deleteEmailAddress: ({ emailAddress }: { emailAddress: string }) => Promise; }; export type IntakeEmailDriverFactory = (args: { config: Config }) => IntakeEmailsServices; diff --git a/apps/papra-server/src/modules/intake-emails/drivers/owlrelay/owlrelay.intake-email-driver.ts b/apps/papra-server/src/modules/intake-emails/drivers/owlrelay/owlrelay.intake-email-driver.ts index 54e51b6..3063c29 100644 --- a/apps/papra-server/src/modules/intake-emails/drivers/owlrelay/owlrelay.intake-email-driver.ts +++ b/apps/papra-server/src/modules/intake-emails/drivers/owlrelay/owlrelay.intake-email-driver.ts @@ -1,12 +1,15 @@ -import { buildUrl } from '@corentinth/chisels'; +import { buildUrl, safely } from '@corentinth/chisels'; import { generateId as generateHumanReadableId } from '@corentinth/friendly-ids'; import { createClient } from '@owlrelay/api-sdk'; +import { createLogger } from '../../../shared/logger/logger'; import { INTAKE_EMAILS_INGEST_ROUTE } from '../../intake-emails.constants'; import { buildEmailAddress } from '../../intake-emails.models'; import { defineIntakeEmailDriver } from '../intake-emails.drivers.models'; export const OWLRELAY_INTAKE_EMAIL_DRIVER_NAME = 'owlrelay'; +const logger = createLogger({ namespace: 'intake-emails.drivers.owlrelay' }); + export const owlrelayIntakeEmailDriverFactory = defineIntakeEmailDriver(({ config }) => { const { baseUrl } = config.server; const { webhookSecret } = config.intakeEmails; @@ -21,7 +24,7 @@ export const owlrelayIntakeEmailDriverFactory = defineIntakeEmailDriver(({ confi return { name: OWLRELAY_INTAKE_EMAIL_DRIVER_NAME, generateEmailAddress: async () => { - const { domain, username } = await client.createEmail({ + const { domain, username, id: owlrelayEmailId } = await client.createEmail({ username: generateHumanReadableId(), webhookUrl, webhookSecret, @@ -29,9 +32,21 @@ export const owlrelayIntakeEmailDriverFactory = defineIntakeEmailDriver(({ confi const emailAddress = buildEmailAddress({ username, domain }); + logger.info({ emailAddress, owlrelayEmailId }, 'Created email address in OwlRelay'); + return { emailAddress, }; }, + deleteEmailAddress: async ({ emailAddress }) => { + const [, error] = await safely(client.deleteEmail({ emailAddress })); + + if (error) { + logger.error({ error }, 'Failed to delete email address in OwlRelay'); + return; + } + + logger.info({ emailAddress }, 'Deleted email address in OwlRelay'); + }, }; }); diff --git a/apps/papra-server/src/modules/intake-emails/drivers/random-username/random-username.intake-email-driver.ts b/apps/papra-server/src/modules/intake-emails/drivers/random-username/random-username.intake-email-driver.ts index c87d1e1..d9e3b66 100644 --- a/apps/papra-server/src/modules/intake-emails/drivers/random-username/random-username.intake-email-driver.ts +++ b/apps/papra-server/src/modules/intake-emails/drivers/random-username/random-username.intake-email-driver.ts @@ -15,5 +15,7 @@ export const randomUsernameIntakeEmailDriverFactory = defineIntakeEmailDriver(({ emailAddress: `${randomUsername}@${domain}`, }; }, + // Deletion functionality is not required for this driver + deleteEmailAddress: async () => {}, }; }); diff --git a/apps/papra-server/src/modules/intake-emails/intake-emails.errors.ts b/apps/papra-server/src/modules/intake-emails/intake-emails.errors.ts index 95c8f85..198d9a9 100644 --- a/apps/papra-server/src/modules/intake-emails/intake-emails.errors.ts +++ b/apps/papra-server/src/modules/intake-emails/intake-emails.errors.ts @@ -5,3 +5,9 @@ export const createIntakeEmailLimitReachedError = createErrorFactory({ code: 'intake_email.limit_reached', statusCode: 403, }); + +export const createIntakeEmailNotFoundError = createErrorFactory({ + message: 'Intake email not found', + code: 'intake_email.not_found', + statusCode: 404, +}); diff --git a/apps/papra-server/src/modules/intake-emails/intake-emails.repository.test.ts b/apps/papra-server/src/modules/intake-emails/intake-emails.repository.test.ts index 53b6a4a..82da1ec 100644 --- a/apps/papra-server/src/modules/intake-emails/intake-emails.repository.test.ts +++ b/apps/papra-server/src/modules/intake-emails/intake-emails.repository.test.ts @@ -44,6 +44,7 @@ describe('intake-emails repository', () => { const { intakeEmail: retrievedIntakeEmail } = await intakeEmailsRepository.getIntakeEmail({ intakeEmailId: intakeEmail.id, + organizationId: 'organization-1', }); expect( diff --git a/apps/papra-server/src/modules/intake-emails/intake-emails.repository.ts b/apps/papra-server/src/modules/intake-emails/intake-emails.repository.ts index 8083289..5ee3d62 100644 --- a/apps/papra-server/src/modules/intake-emails/intake-emails.repository.ts +++ b/apps/papra-server/src/modules/intake-emails/intake-emails.repository.ts @@ -47,12 +47,15 @@ async function updateIntakeEmail({ intakeEmailId, organizationId, isEnabled, all return { intakeEmail }; } -async function getIntakeEmail({ intakeEmailId, db }: { intakeEmailId: string; db: Database }) { +async function getIntakeEmail({ intakeEmailId, organizationId, db }: { intakeEmailId: string; organizationId: string; db: Database }) { const [intakeEmail] = await db .select() .from(intakeEmailsTable) .where( - eq(intakeEmailsTable.id, intakeEmailId), + and( + eq(intakeEmailsTable.id, intakeEmailId), + eq(intakeEmailsTable.organizationId, organizationId), + ), ); return { intakeEmail }; diff --git a/apps/papra-server/src/modules/intake-emails/intake-emails.routes.ts b/apps/papra-server/src/modules/intake-emails/intake-emails.routes.ts index b335ffe..6f9bc8b 100644 --- a/apps/papra-server/src/modules/intake-emails/intake-emails.routes.ts +++ b/apps/papra-server/src/modules/intake-emails/intake-emails.routes.ts @@ -18,7 +18,7 @@ import { INTAKE_EMAILS_INGEST_ROUTE } from './intake-emails.constants'; import { createIntakeEmailsRepository } from './intake-emails.repository'; import { intakeEmailsIngestionMetaSchema, parseJson } from './intake-emails.schemas'; import { createIntakeEmailsServices } from './intake-emails.services'; -import { createIntakeEmail, processIntakeEmailIngestion } from './intake-emails.usecases'; +import { createIntakeEmail, deleteIntakeEmail, processIntakeEmailIngestion } from './intake-emails.usecases'; const logger = createLogger({ namespace: 'intake-emails.routes' }); @@ -86,7 +86,7 @@ function setupCreateIntakeEmailRoute({ app, db, config }: RouteDefinitionContext ); } -function setupDeleteIntakeEmailRoute({ app, db }: RouteDefinitionContext) { +function setupDeleteIntakeEmailRoute({ app, db, config }: RouteDefinitionContext) { app.delete( '/api/organizations/:organizationId/intake-emails/:intakeEmailId', validateParams(z.object({ @@ -99,10 +99,11 @@ function setupDeleteIntakeEmailRoute({ app, db }: RouteDefinitionContext) { const organizationsRepository = createOrganizationsRepository({ db }); const intakeEmailsRepository = createIntakeEmailsRepository({ db }); + const intakeEmailsServices = createIntakeEmailsServices({ config }); await ensureUserIsInOrganization({ userId, organizationId, organizationsRepository }); - await intakeEmailsRepository.deleteIntakeEmail({ intakeEmailId, organizationId }); + await deleteIntakeEmail({ intakeEmailId, organizationId, intakeEmailsRepository, intakeEmailsServices }); return context.body(null, 204); }, diff --git a/apps/papra-server/src/modules/intake-emails/intake-emails.usecases.ts b/apps/papra-server/src/modules/intake-emails/intake-emails.usecases.ts index a0caba2..68bdfdc 100644 --- a/apps/papra-server/src/modules/intake-emails/intake-emails.usecases.ts +++ b/apps/papra-server/src/modules/intake-emails/intake-emails.usecases.ts @@ -10,7 +10,7 @@ import { safely } from '@corentinth/chisels'; import { createDocument } from '../documents/documents.usecases'; import { getOrganizationPlan } from '../plans/plans.usecases'; import { addLogContext, createLogger } from '../shared/logger/logger'; -import { createIntakeEmailLimitReachedError } from './intake-emails.errors'; +import { createIntakeEmailLimitReachedError, createIntakeEmailNotFoundError } from './intake-emails.errors'; import { getIsFromAllowedOrigin } from './intake-emails.models'; export async function createIntakeEmail({ @@ -165,3 +165,24 @@ export async function checkIfOrganizationCanCreateNewIntakeEmail({ throw createIntakeEmailLimitReachedError(); } } + +export async function deleteIntakeEmail({ + intakeEmailId, + organizationId, + intakeEmailsRepository, + intakeEmailsServices, +}: { + intakeEmailId: string; + organizationId: string; + intakeEmailsRepository: IntakeEmailsRepository; + intakeEmailsServices: IntakeEmailsServices; +}) { + const { intakeEmail } = await intakeEmailsRepository.getIntakeEmail({ intakeEmailId, organizationId }); + + if (!intakeEmail) { + throw createIntakeEmailNotFoundError(); + } + + await intakeEmailsRepository.deleteIntakeEmail({ organizationId: intakeEmail.organizationId, intakeEmailId }); + await intakeEmailsServices.deleteEmailAddress({ emailAddress: intakeEmail.emailAddress }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 912c906..dc34593 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,8 +236,8 @@ importers: specifier: ^0.14.0 version: 0.14.0 '@owlrelay/api-sdk': - specifier: ^0.0.1 - version: 0.0.1 + specifier: ^0.0.2 + version: 0.0.2 '@owlrelay/webhook': specifier: ^0.0.3 version: 0.0.3 @@ -2064,8 +2064,8 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@owlrelay/api-sdk@0.0.1': - resolution: {integrity: sha512-/4x/J4ktb9z3Zf3VfX7tjQYYqgn8bMXk7hbYePoGknijk90QRjsfsVjpPjl2fgUs+ZGFeOQqdsgLmpxq2d1NGA==} + '@owlrelay/api-sdk@0.0.2': + resolution: {integrity: sha512-7WzTd/IKiT/h9Y90O7aCdrdZVk26Tb4f4EkIGRzzF0hvujmrp98R3TNji/ZD9As0v6OOilMlMYm4xlZGQzu+bQ==} engines: {node: '>=20.0.0'} '@owlrelay/webhook@0.0.3': @@ -8551,7 +8551,7 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@owlrelay/api-sdk@0.0.1': + '@owlrelay/api-sdk@0.0.2': dependencies: ofetch: 1.4.1