feat: CalDav Driver (#13170)

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
neo773
2025-07-15 21:11:23 +05:30
committed by GitHub
parent c5a74b8e92
commit 3e8fa3120d
22 changed files with 1210 additions and 339 deletions

View File

@ -14,6 +14,9 @@ export class ConnectionParameters {
@Field(() => Number)
port: number;
@Field(() => String, { nullable: true })
username?: string;
/**
* Note: This field is stored in plain text in the database.
* While encrypting it could provide an extra layer of defense, we have decided not to,
@ -26,6 +29,18 @@ export class ConnectionParameters {
secure?: boolean;
}
@InputType()
export class EmailAccountConnectionParameters {
@Field(() => ConnectionParameters, { nullable: true })
IMAP?: ConnectionParameters;
@Field(() => ConnectionParameters, { nullable: true })
SMTP?: ConnectionParameters;
@Field(() => ConnectionParameters, { nullable: true })
CALDAV?: ConnectionParameters;
}
@ObjectType()
export class ConnectionParametersOutput {
@Field(() => String)
@ -34,6 +49,9 @@ export class ConnectionParametersOutput {
@Field(() => Number)
port: number;
@Field(() => String, { nullable: true })
username?: string;
@Field(() => String)
password: string;

View File

@ -16,10 +16,7 @@ import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/re
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ConnectedImapSmtpCaldavAccount } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connected-account.dto';
import { ImapSmtpCaldavConnectionSuccess } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection-success.dto';
import {
AccountType,
ConnectionParameters,
} from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto';
import { EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto';
import { ImapSmtpCaldavValidatorService } from 'src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection-validator.service';
import { ImapSmtpCaldavService } from 'src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -78,12 +75,11 @@ export class ImapSmtpCaldavResolver {
@Mutation(() => ImapSmtpCaldavConnectionSuccess)
@UseGuards(WorkspaceAuthGuard)
async saveImapSmtpCaldav(
async saveImapSmtpCaldavAccount(
@Args('accountOwnerId') accountOwnerId: string,
@Args('handle') handle: string,
@Args('accountType') accountType: AccountType,
@Args('connectionParameters')
connectionParameters: ConnectionParameters,
connectionParameters: EmailAccountConnectionParameters,
@AuthWorkspace() workspace: Workspace,
@Args('id', { nullable: true }) id?: string,
): Promise<ImapSmtpCaldavConnectionSuccess> {
@ -100,23 +96,16 @@ export class ImapSmtpCaldavResolver {
);
}
const validatedParams =
this.mailConnectionValidatorService.validateProtocolConnectionParams(
connectionParameters,
);
await this.ImapSmtpCaldavConnectionService.testImapSmtpCaldav(
const validatedParams = await this.validateAndTestConnectionParameters(
connectionParameters,
handle,
validatedParams,
accountType.type,
);
await this.imapSmtpCaldavApisService.setupConnectedAccount({
await this.imapSmtpCaldavApisService.setupCompleteAccount({
handle,
workspaceMemberId: accountOwnerId,
workspaceId: workspace.id,
connectionParams: validatedParams,
accountType: accountType.type,
connectionParameters: validatedParams,
connectedAccountId: id,
});
@ -124,4 +113,34 @@ export class ImapSmtpCaldavResolver {
success: true,
};
}
private async validateAndTestConnectionParameters(
connectionParameters: EmailAccountConnectionParameters,
handle: string,
): Promise<EmailAccountConnectionParameters> {
const validatedParams: EmailAccountConnectionParameters = {};
const protocols = ['IMAP', 'SMTP', 'CALDAV'] as const;
for (const protocol of protocols) {
const params = connectionParameters[protocol];
if (params) {
validatedParams[protocol] =
this.mailConnectionValidatorService.validateProtocolConnectionParams(
params,
);
const validatedProtocolParams = validatedParams[protocol];
if (validatedProtocolParams) {
await this.ImapSmtpCaldavConnectionService.testImapSmtpCaldav(
handle,
validatedProtocolParams,
protocol,
);
}
}
}
return validatedParams;
}
}

View File

@ -10,6 +10,7 @@ export class ImapSmtpCaldavValidatorService {
private readonly protocolConnectionSchema = z.object({
host: z.string().min(1, 'Host is required'),
port: z.number().int().positive('Port must be a positive number'),
username: z.string().optional(),
password: z.string().min(1, 'Password is required'),
secure: z.boolean().optional(),
});

View File

@ -10,6 +10,7 @@ import {
ConnectionParameters,
} from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { CalDAVClient } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
@ -121,7 +122,31 @@ export class ImapSmtpCaldavService {
handle: string,
params: ConnectionParameters,
): Promise<boolean> {
this.logger.log('CALDAV connection testing not yet implemented', params);
const client = new CalDAVClient({
serverUrl: params.host,
username: params.username ?? handle,
password: params.password,
});
try {
await client.listCalendars();
} catch (error) {
this.logger.error(
`CALDAV connection failed: ${error.message}`,
error.stack,
);
if (error.code === 'FailedToOpenSocket') {
throw new UserInputError(`CALDAV connection failed: ${error.message}`, {
userFriendlyMessage:
"We couldn't connect to your CalDAV server. Please check your server settings and try again.",
});
}
throw new UserInputError(`CALDAV connection failed: ${error.message}`, {
userFriendlyMessage:
'Invalid credentials. Please check your username and password.',
});
}
return true;
}

View File

@ -1,6 +1,7 @@
export type ConnectionParameters = {
host: string;
port: number;
username?: string;
password: string;
secure?: boolean;
};