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:
@ -22,10 +22,6 @@ export type Scalars = {
|
|||||||
Upload: any;
|
Upload: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AccountType = {
|
|
||||||
type: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ActivateWorkspaceInput = {
|
export type ActivateWorkspaceInput = {
|
||||||
displayName?: InputMaybe<Scalars['String']>;
|
displayName?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
@ -435,6 +431,7 @@ export type ConnectionParameters = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: InputMaybe<Scalars['Boolean']>;
|
secure?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
username?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConnectionParametersOutput = {
|
export type ConnectionParametersOutput = {
|
||||||
@ -443,6 +440,7 @@ export type ConnectionParametersOutput = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: Maybe<Scalars['Boolean']>;
|
secure?: Maybe<Scalars['Boolean']>;
|
||||||
|
username?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateApiKeyDto = {
|
export type CreateApiKeyDto = {
|
||||||
@ -673,6 +671,12 @@ export type EditSsoOutput = {
|
|||||||
type: IdentityProviderType;
|
type: IdentityProviderType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EmailAccountConnectionParameters = {
|
||||||
|
CALDAV?: InputMaybe<ConnectionParameters>;
|
||||||
|
IMAP?: InputMaybe<ConnectionParameters>;
|
||||||
|
SMTP?: InputMaybe<ConnectionParameters>;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmailPasswordResetLink = {
|
export type EmailPasswordResetLink = {
|
||||||
__typename?: 'EmailPasswordResetLink';
|
__typename?: 'EmailPasswordResetLink';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -1117,7 +1121,7 @@ export type Mutation = {
|
|||||||
resendWorkspaceInvitation: SendInvitationsOutput;
|
resendWorkspaceInvitation: SendInvitationsOutput;
|
||||||
revokeApiKey?: Maybe<ApiKey>;
|
revokeApiKey?: Maybe<ApiKey>;
|
||||||
runWorkflowVersion: WorkflowRun;
|
runWorkflowVersion: WorkflowRun;
|
||||||
saveImapSmtpCaldav: ImapSmtpCaldavConnectionSuccess;
|
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
|
||||||
sendInvitations: SendInvitationsOutput;
|
sendInvitations: SendInvitationsOutput;
|
||||||
signIn: AvailableWorkspacesAndAccessTokensOutput;
|
signIn: AvailableWorkspacesAndAccessTokensOutput;
|
||||||
signUp: AvailableWorkspacesAndAccessTokensOutput;
|
signUp: AvailableWorkspacesAndAccessTokensOutput;
|
||||||
@ -1436,10 +1440,9 @@ export type MutationRunWorkflowVersionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationSaveImapSmtpCaldavArgs = {
|
export type MutationSaveImapSmtpCaldavAccountArgs = {
|
||||||
accountOwnerId: Scalars['String'];
|
accountOwnerId: Scalars['String'];
|
||||||
accountType: AccountType;
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
connectionParameters: ConnectionParameters;
|
|
||||||
handle: Scalars['String'];
|
handle: Scalars['String'];
|
||||||
id?: InputMaybe<Scalars['String']>;
|
id?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
@ -3291,23 +3294,22 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]
|
|||||||
|
|
||||||
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
|
||||||
|
|
||||||
export type SaveImapSmtpCaldavMutationVariables = Exact<{
|
export type SaveImapSmtpCaldavAccountMutationVariables = Exact<{
|
||||||
accountOwnerId: Scalars['String'];
|
accountOwnerId: Scalars['String'];
|
||||||
handle: Scalars['String'];
|
handle: Scalars['String'];
|
||||||
accountType: AccountType;
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
connectionParameters: ConnectionParameters;
|
|
||||||
id?: InputMaybe<Scalars['String']>;
|
id?: InputMaybe<Scalars['String']>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type SaveImapSmtpCaldavMutation = { __typename?: 'Mutation', saveImapSmtpCaldav: { __typename?: 'ImapSmtpCaldavConnectionSuccess', success: boolean } };
|
export type SaveImapSmtpCaldavAccountMutation = { __typename?: 'Mutation', saveImapSmtpCaldavAccount: { __typename?: 'ImapSmtpCaldavConnectionSuccess', success: boolean } };
|
||||||
|
|
||||||
export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
|
export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
|
||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null } | null } };
|
export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } };
|
||||||
|
|
||||||
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
|
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
|
||||||
key: Scalars['String'];
|
key: Scalars['String'];
|
||||||
@ -6061,12 +6063,11 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta
|
|||||||
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType<typeof useSkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult<SkipSyncEmailOnboardingStepMutation>;
|
||||||
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions<SkipSyncEmailOnboardingStepMutation, SkipSyncEmailOnboardingStepMutationVariables>;
|
||||||
export const SaveImapSmtpCaldavDocument = gql`
|
export const SaveImapSmtpCaldavAccountDocument = gql`
|
||||||
mutation SaveImapSmtpCaldav($accountOwnerId: String!, $handle: String!, $accountType: AccountType!, $connectionParameters: ConnectionParameters!, $id: String) {
|
mutation SaveImapSmtpCaldavAccount($accountOwnerId: String!, $handle: String!, $connectionParameters: EmailAccountConnectionParameters!, $id: String) {
|
||||||
saveImapSmtpCaldav(
|
saveImapSmtpCaldavAccount(
|
||||||
accountOwnerId: $accountOwnerId
|
accountOwnerId: $accountOwnerId
|
||||||
handle: $handle
|
handle: $handle
|
||||||
accountType: $accountType
|
|
||||||
connectionParameters: $connectionParameters
|
connectionParameters: $connectionParameters
|
||||||
id: $id
|
id: $id
|
||||||
) {
|
) {
|
||||||
@ -6074,36 +6075,35 @@ export const SaveImapSmtpCaldavDocument = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export type SaveImapSmtpCaldavMutationFn = Apollo.MutationFunction<SaveImapSmtpCaldavMutation, SaveImapSmtpCaldavMutationVariables>;
|
export type SaveImapSmtpCaldavAccountMutationFn = Apollo.MutationFunction<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useSaveImapSmtpCaldavMutation__
|
* __useSaveImapSmtpCaldavAccountMutation__
|
||||||
*
|
*
|
||||||
* To run a mutation, you first call `useSaveImapSmtpCaldavMutation` within a React component and pass it any options that fit your needs.
|
* To run a mutation, you first call `useSaveImapSmtpCaldavAccountMutation` within a React component and pass it any options that fit your needs.
|
||||||
* When your component renders, `useSaveImapSmtpCaldavMutation` returns a tuple that includes:
|
* When your component renders, `useSaveImapSmtpCaldavAccountMutation` returns a tuple that includes:
|
||||||
* - A mutate function that you can call at any time to execute the mutation
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
* - An object with fields that represent the current status of the mutation's execution
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
*
|
*
|
||||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const [saveImapSmtpCaldavMutation, { data, loading, error }] = useSaveImapSmtpCaldavMutation({
|
* const [saveImapSmtpCaldavAccountMutation, { data, loading, error }] = useSaveImapSmtpCaldavAccountMutation({
|
||||||
* variables: {
|
* variables: {
|
||||||
* accountOwnerId: // value for 'accountOwnerId'
|
* accountOwnerId: // value for 'accountOwnerId'
|
||||||
* handle: // value for 'handle'
|
* handle: // value for 'handle'
|
||||||
* accountType: // value for 'accountType'
|
|
||||||
* connectionParameters: // value for 'connectionParameters'
|
* connectionParameters: // value for 'connectionParameters'
|
||||||
* id: // value for 'id'
|
* id: // value for 'id'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function useSaveImapSmtpCaldavMutation(baseOptions?: Apollo.MutationHookOptions<SaveImapSmtpCaldavMutation, SaveImapSmtpCaldavMutationVariables>) {
|
export function useSaveImapSmtpCaldavAccountMutation(baseOptions?: Apollo.MutationHookOptions<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useMutation<SaveImapSmtpCaldavMutation, SaveImapSmtpCaldavMutationVariables>(SaveImapSmtpCaldavDocument, options);
|
return Apollo.useMutation<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>(SaveImapSmtpCaldavAccountDocument, options);
|
||||||
}
|
}
|
||||||
export type SaveImapSmtpCaldavMutationHookResult = ReturnType<typeof useSaveImapSmtpCaldavMutation>;
|
export type SaveImapSmtpCaldavAccountMutationHookResult = ReturnType<typeof useSaveImapSmtpCaldavAccountMutation>;
|
||||||
export type SaveImapSmtpCaldavMutationResult = Apollo.MutationResult<SaveImapSmtpCaldavMutation>;
|
export type SaveImapSmtpCaldavAccountMutationResult = Apollo.MutationResult<SaveImapSmtpCaldavAccountMutation>;
|
||||||
export type SaveImapSmtpCaldavMutationOptions = Apollo.BaseMutationOptions<SaveImapSmtpCaldavMutation, SaveImapSmtpCaldavMutationVariables>;
|
export type SaveImapSmtpCaldavAccountMutationOptions = Apollo.BaseMutationOptions<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>;
|
||||||
export const GetConnectedImapSmtpCaldavAccountDocument = gql`
|
export const GetConnectedImapSmtpCaldavAccountDocument = gql`
|
||||||
query GetConnectedImapSmtpCaldavAccount($id: String!) {
|
query GetConnectedImapSmtpCaldavAccount($id: String!) {
|
||||||
getConnectedImapSmtpCaldavAccount(id: $id) {
|
getConnectedImapSmtpCaldavAccount(id: $id) {
|
||||||
@ -6126,8 +6126,7 @@ export const GetConnectedImapSmtpCaldavAccountDocument = gql`
|
|||||||
}
|
}
|
||||||
CALDAV {
|
CALDAV {
|
||||||
host
|
host
|
||||||
port
|
username
|
||||||
secure
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,10 +22,6 @@ export type Scalars = {
|
|||||||
Upload: any;
|
Upload: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AccountType = {
|
|
||||||
type: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ActivateWorkspaceInput = {
|
export type ActivateWorkspaceInput = {
|
||||||
displayName?: InputMaybe<Scalars['String']>;
|
displayName?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
@ -435,6 +431,7 @@ export type ConnectionParameters = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: InputMaybe<Scalars['Boolean']>;
|
secure?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
username?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConnectionParametersOutput = {
|
export type ConnectionParametersOutput = {
|
||||||
@ -443,6 +440,7 @@ export type ConnectionParametersOutput = {
|
|||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
port: Scalars['Float'];
|
port: Scalars['Float'];
|
||||||
secure?: Maybe<Scalars['Boolean']>;
|
secure?: Maybe<Scalars['Boolean']>;
|
||||||
|
username?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateApiKeyDto = {
|
export type CreateApiKeyDto = {
|
||||||
@ -637,6 +635,12 @@ export type EditSsoOutput = {
|
|||||||
type: IdentityProviderType;
|
type: IdentityProviderType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EmailAccountConnectionParameters = {
|
||||||
|
CALDAV?: InputMaybe<ConnectionParameters>;
|
||||||
|
IMAP?: InputMaybe<ConnectionParameters>;
|
||||||
|
SMTP?: InputMaybe<ConnectionParameters>;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmailPasswordResetLink = {
|
export type EmailPasswordResetLink = {
|
||||||
__typename?: 'EmailPasswordResetLink';
|
__typename?: 'EmailPasswordResetLink';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -1072,7 +1076,7 @@ export type Mutation = {
|
|||||||
resendWorkspaceInvitation: SendInvitationsOutput;
|
resendWorkspaceInvitation: SendInvitationsOutput;
|
||||||
revokeApiKey?: Maybe<ApiKey>;
|
revokeApiKey?: Maybe<ApiKey>;
|
||||||
runWorkflowVersion: WorkflowRun;
|
runWorkflowVersion: WorkflowRun;
|
||||||
saveImapSmtpCaldav: ImapSmtpCaldavConnectionSuccess;
|
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
|
||||||
sendInvitations: SendInvitationsOutput;
|
sendInvitations: SendInvitationsOutput;
|
||||||
signIn: AvailableWorkspacesAndAccessTokensOutput;
|
signIn: AvailableWorkspacesAndAccessTokensOutput;
|
||||||
signUp: AvailableWorkspacesAndAccessTokensOutput;
|
signUp: AvailableWorkspacesAndAccessTokensOutput;
|
||||||
@ -1367,10 +1371,9 @@ export type MutationRunWorkflowVersionArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationSaveImapSmtpCaldavArgs = {
|
export type MutationSaveImapSmtpCaldavAccountArgs = {
|
||||||
accountOwnerId: Scalars['String'];
|
accountOwnerId: Scalars['String'];
|
||||||
accountType: AccountType;
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
connectionParameters: ConnectionParameters;
|
|
||||||
handle: Scalars['String'];
|
handle: Scalars['String'];
|
||||||
id?: InputMaybe<Scalars['String']>;
|
id?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -291,6 +291,22 @@ export const SettingsAccountsConnectionForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="CALDAV.username"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextInput
|
||||||
|
instanceId="caldav-username-connection-form"
|
||||||
|
label={t`CalDAV Username`}
|
||||||
|
placeholder={t`john.doe`}
|
||||||
|
required={false}
|
||||||
|
value={field.value || ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="CALDAV.password"
|
name="CALDAV.password"
|
||||||
control={control}
|
control={control}
|
||||||
@ -306,47 +322,6 @@ export const SettingsAccountsConnectionForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StyledFieldRow>
|
|
||||||
<StyledFieldGroup>
|
|
||||||
<Controller
|
|
||||||
name="CALDAV.port"
|
|
||||||
control={control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<TextInput
|
|
||||||
instanceId="caldav-port-connection-form"
|
|
||||||
label={t`CalDAV Port`}
|
|
||||||
type="number"
|
|
||||||
placeholder="443"
|
|
||||||
value={field?.value ? field.value : 443}
|
|
||||||
onChange={(value) =>
|
|
||||||
field.onChange(handlePortChange(value))
|
|
||||||
}
|
|
||||||
error={fieldState.error?.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</StyledFieldGroup>
|
|
||||||
|
|
||||||
<StyledFieldGroup>
|
|
||||||
<Controller
|
|
||||||
name="CALDAV.secure"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
label={t`CalDAV Encryption`}
|
|
||||||
options={[
|
|
||||||
{ label: 'SSL/TLS', value: true },
|
|
||||||
{ label: 'None', value: false },
|
|
||||||
]}
|
|
||||||
value={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
dropdownId="caldav-secure-dropdown"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</StyledFieldGroup>
|
|
||||||
</StyledFieldRow>
|
|
||||||
</StyledConnectionSection>
|
</StyledConnectionSection>
|
||||||
</StyledFormContainer>
|
</StyledFormContainer>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
export const SAVE_IMAP_SMTP_CALDAV_CONNECTION = gql`
|
export const SAVE_IMAP_SMTP_CALDAV_ACCOUNT = gql`
|
||||||
mutation SaveImapSmtpCaldav(
|
mutation SaveImapSmtpCaldavAccount(
|
||||||
$accountOwnerId: String!
|
$accountOwnerId: String!
|
||||||
$handle: String!
|
$handle: String!
|
||||||
$accountType: AccountType!
|
$connectionParameters: EmailAccountConnectionParameters!
|
||||||
$connectionParameters: ConnectionParameters!
|
|
||||||
$id: String
|
$id: String
|
||||||
) {
|
) {
|
||||||
saveImapSmtpCaldav(
|
saveImapSmtpCaldavAccount(
|
||||||
accountOwnerId: $accountOwnerId
|
accountOwnerId: $accountOwnerId
|
||||||
handle: $handle
|
handle: $handle
|
||||||
accountType: $accountType
|
|
||||||
connectionParameters: $connectionParameters
|
connectionParameters: $connectionParameters
|
||||||
id: $id
|
id: $id
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -22,8 +22,7 @@ export const GET_CONNECTED_IMAP_SMTP_CALDAV_ACCOUNT = gql`
|
|||||||
}
|
}
|
||||||
CALDAV {
|
CALDAV {
|
||||||
host
|
host
|
||||||
port
|
username
|
||||||
secure
|
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import {
|
import {
|
||||||
ConnectionParameters,
|
ConnectionParameters,
|
||||||
useSaveImapSmtpCaldavMutation,
|
useSaveImapSmtpCaldavAccountMutation,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
|
|
||||||
@ -49,7 +49,13 @@ export const useImapSmtpCaldavConnectionForm = ({
|
|||||||
handle: '',
|
handle: '',
|
||||||
IMAP: { host: '', port: 993, password: '', secure: true },
|
IMAP: { host: '', port: 993, password: '', secure: true },
|
||||||
SMTP: { host: '', port: 587, password: '', secure: true },
|
SMTP: { host: '', port: 587, password: '', secure: true },
|
||||||
CALDAV: { host: '', port: 443, password: '', secure: true },
|
CALDAV: {
|
||||||
|
host: '',
|
||||||
|
port: 443,
|
||||||
|
password: '',
|
||||||
|
secure: true,
|
||||||
|
username: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,7 +82,7 @@ export const useImapSmtpCaldavConnectionForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [saveConnection, { loading: saveLoading }] =
|
const [saveConnection, { loading: saveLoading }] =
|
||||||
useSaveImapSmtpCaldavMutation();
|
useSaveImapSmtpCaldavAccountMutation();
|
||||||
|
|
||||||
const watchedValues = watch();
|
const watchedValues = watch();
|
||||||
|
|
||||||
@ -102,47 +108,39 @@ export const useImapSmtpCaldavConnectionForm = ({
|
|||||||
);
|
);
|
||||||
}, [getConfiguredProtocols, watchedValues.handle]);
|
}, [getConfiguredProtocols, watchedValues.handle]);
|
||||||
|
|
||||||
const saveIndividualConnection = useCallback(
|
const handleSave = useCallback(
|
||||||
async (
|
async (formValues: ConnectionFormData): Promise<void> => {
|
||||||
protocol: keyof ImapSmtpCaldavAccount,
|
|
||||||
formValues: ConnectionFormData,
|
|
||||||
): Promise<void> => {
|
|
||||||
if (!currentWorkspaceMember?.id) {
|
if (!currentWorkspaceMember?.id) {
|
||||||
throw new Error('Workspace member ID is missing');
|
throw new Error('Workspace member ID is missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocolConfig = formValues[protocol];
|
|
||||||
if (!protocolConfig) {
|
|
||||||
throw new Error(`${protocol} configuration is missing`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveConnection({
|
|
||||||
variables: {
|
|
||||||
...(isEditing && connectedAccountId
|
|
||||||
? { id: connectedAccountId }
|
|
||||||
: {}),
|
|
||||||
accountOwnerId: currentWorkspaceMember.id,
|
|
||||||
handle: formValues.handle,
|
|
||||||
accountType: {
|
|
||||||
type: protocol,
|
|
||||||
},
|
|
||||||
connectionParameters: protocolConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[saveConnection, isEditing, connectedAccountId, currentWorkspaceMember?.id],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSave = useCallback(
|
|
||||||
async (formValues: ConnectionFormData): Promise<void> => {
|
|
||||||
const configuredProtocols = getConfiguredProtocols(formValues);
|
const configuredProtocols = getConfiguredProtocols(formValues);
|
||||||
|
|
||||||
|
if (configuredProtocols.length === 0) {
|
||||||
|
throw new Error('At least one protocol must be configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionParameters: Partial<
|
||||||
|
Record<keyof ImapSmtpCaldavAccount, ConnectionParameters>
|
||||||
|
> = {};
|
||||||
|
configuredProtocols.forEach((protocol) => {
|
||||||
|
const protocolConfig = formValues[protocol];
|
||||||
|
if (isDefined(protocolConfig)) {
|
||||||
|
connectionParameters[protocol] = protocolConfig;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await saveConnection({
|
||||||
configuredProtocols.map((protocol) =>
|
variables: {
|
||||||
saveIndividualConnection(protocol, formValues),
|
...(isEditing && connectedAccountId
|
||||||
),
|
? { id: connectedAccountId }
|
||||||
);
|
: {}),
|
||||||
|
accountOwnerId: currentWorkspaceMember.id,
|
||||||
|
handle: formValues.handle,
|
||||||
|
connectionParameters,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const successMessage = isEditing
|
const successMessage = isEditing
|
||||||
? t`Connection successfully updated`
|
? t`Connection successfully updated`
|
||||||
@ -160,12 +158,14 @@ export const useImapSmtpCaldavConnectionForm = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
currentWorkspaceMember?.id,
|
||||||
getConfiguredProtocols,
|
getConfiguredProtocols,
|
||||||
saveIndividualConnection,
|
saveConnection,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
connectedAccountId,
|
||||||
enqueueSuccessSnackBar,
|
enqueueSuccessSnackBar,
|
||||||
enqueueErrorSnackBar,
|
|
||||||
navigate,
|
navigate,
|
||||||
|
enqueueErrorSnackBar,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ const connectionParameters = z
|
|||||||
.object({
|
.object({
|
||||||
host: z.string().default(''),
|
host: z.string().default(''),
|
||||||
port: z.number().int().nullable().default(null),
|
port: z.number().int().nullable().default(null),
|
||||||
|
username: z.string().optional(),
|
||||||
password: z.string().default(''),
|
password: z.string().default(''),
|
||||||
secure: z.boolean().default(true),
|
secure: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -54,6 +54,7 @@
|
|||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
"monaco-editor": "^0.51.0",
|
"monaco-editor": "^0.51.0",
|
||||||
"monaco-editor-auto-typings": "^0.4.5",
|
"monaco-editor-auto-typings": "^0.4.5",
|
||||||
|
"node-ical": "^0.20.1",
|
||||||
"openid-client": "^5.7.0",
|
"openid-client": "^5.7.0",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
@ -61,6 +62,7 @@
|
|||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"ts-morph": "^24.0.0",
|
"ts-morph": "^24.0.0",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"tsdav": "^2.1.5",
|
||||||
"typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch",
|
"typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch",
|
||||||
"unzipper": "^0.12.3",
|
"unzipper": "^0.12.3",
|
||||||
"zod-to-json-schema": "^3.23.1"
|
"zod-to-json-schema": "^3.23.1"
|
||||||
|
|||||||
@ -14,6 +14,9 @@ export class ConnectionParameters {
|
|||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
port: number;
|
port: number;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: This field is stored in plain text in the database.
|
* 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,
|
* While encrypting it could provide an extra layer of defense, we have decided not to,
|
||||||
@ -26,6 +29,18 @@ export class ConnectionParameters {
|
|||||||
secure?: boolean;
|
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()
|
@ObjectType()
|
||||||
export class ConnectionParametersOutput {
|
export class ConnectionParametersOutput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@ -34,6 +49,9 @@ export class ConnectionParametersOutput {
|
|||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
port: number;
|
port: number;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
|||||||
@ -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 { 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 { 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 { ImapSmtpCaldavConnectionSuccess } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection-success.dto';
|
||||||
import {
|
import { EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto';
|
||||||
AccountType,
|
|
||||||
ConnectionParameters,
|
|
||||||
} 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 { 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 { 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';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
@ -78,12 +75,11 @@ export class ImapSmtpCaldavResolver {
|
|||||||
|
|
||||||
@Mutation(() => ImapSmtpCaldavConnectionSuccess)
|
@Mutation(() => ImapSmtpCaldavConnectionSuccess)
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
async saveImapSmtpCaldav(
|
async saveImapSmtpCaldavAccount(
|
||||||
@Args('accountOwnerId') accountOwnerId: string,
|
@Args('accountOwnerId') accountOwnerId: string,
|
||||||
@Args('handle') handle: string,
|
@Args('handle') handle: string,
|
||||||
@Args('accountType') accountType: AccountType,
|
|
||||||
@Args('connectionParameters')
|
@Args('connectionParameters')
|
||||||
connectionParameters: ConnectionParameters,
|
connectionParameters: EmailAccountConnectionParameters,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@Args('id', { nullable: true }) id?: string,
|
@Args('id', { nullable: true }) id?: string,
|
||||||
): Promise<ImapSmtpCaldavConnectionSuccess> {
|
): Promise<ImapSmtpCaldavConnectionSuccess> {
|
||||||
@ -100,23 +96,16 @@ export class ImapSmtpCaldavResolver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedParams =
|
const validatedParams = await this.validateAndTestConnectionParameters(
|
||||||
this.mailConnectionValidatorService.validateProtocolConnectionParams(
|
connectionParameters,
|
||||||
connectionParameters,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.ImapSmtpCaldavConnectionService.testImapSmtpCaldav(
|
|
||||||
handle,
|
handle,
|
||||||
validatedParams,
|
|
||||||
accountType.type,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.imapSmtpCaldavApisService.setupConnectedAccount({
|
await this.imapSmtpCaldavApisService.setupCompleteAccount({
|
||||||
handle,
|
handle,
|
||||||
workspaceMemberId: accountOwnerId,
|
workspaceMemberId: accountOwnerId,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
connectionParams: validatedParams,
|
connectionParameters: validatedParams,
|
||||||
accountType: accountType.type,
|
|
||||||
connectedAccountId: id,
|
connectedAccountId: id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -124,4 +113,34 @@ export class ImapSmtpCaldavResolver {
|
|||||||
success: true,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export class ImapSmtpCaldavValidatorService {
|
|||||||
private readonly protocolConnectionSchema = z.object({
|
private readonly protocolConnectionSchema = z.object({
|
||||||
host: z.string().min(1, 'Host is required'),
|
host: z.string().min(1, 'Host is required'),
|
||||||
port: z.number().int().positive('Port must be a positive number'),
|
port: z.number().int().positive('Port must be a positive number'),
|
||||||
|
username: z.string().optional(),
|
||||||
password: z.string().min(1, 'Password is required'),
|
password: z.string().min(1, 'Password is required'),
|
||||||
secure: z.boolean().optional(),
|
secure: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
ConnectionParameters,
|
ConnectionParameters,
|
||||||
} from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
} 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 { 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';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -121,7 +122,31 @@ export class ImapSmtpCaldavService {
|
|||||||
handle: string,
|
handle: string,
|
||||||
params: ConnectionParameters,
|
params: ConnectionParameters,
|
||||||
): Promise<boolean> {
|
): 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export type ConnectionParameters = {
|
export type ConnectionParameters = {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
username?: string;
|
||||||
password: string;
|
password: string;
|
||||||
secure?: boolean;
|
secure?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { CalendarOngoingStaleCronCommand } from 'src/modules/calendar/calendar-e
|
|||||||
import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job';
|
import { CalendarEventListFetchCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job';
|
||||||
import { CalendarEventsImportCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job';
|
import { CalendarEventsImportCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job';
|
||||||
import { CalendarOngoingStaleCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-ongoing-stale.cron.job';
|
import { CalendarOngoingStaleCronJob } from 'src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-ongoing-stale.cron.job';
|
||||||
|
import { CalDavDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/caldav-driver.module';
|
||||||
import { GoogleCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module';
|
import { GoogleCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/google-calendar-driver.module';
|
||||||
import { MicrosoftCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module';
|
import { MicrosoftCalendarDriverModule } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/microsoft-calendar-driver.module';
|
||||||
import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
|
import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
|
||||||
@ -43,6 +44,7 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
|
|||||||
WorkspaceDataSourceModule,
|
WorkspaceDataSourceModule,
|
||||||
CalendarEventCleanerModule,
|
CalendarEventCleanerModule,
|
||||||
GoogleCalendarDriverModule,
|
GoogleCalendarDriverModule,
|
||||||
|
CalDavDriverModule,
|
||||||
MicrosoftCalendarDriverModule,
|
MicrosoftCalendarDriverModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
RefreshTokensManagerModule,
|
RefreshTokensManagerModule,
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||||
|
import { CalDavClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/providers/caldav.provider';
|
||||||
|
import { CalDavGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-get-events.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TwentyConfigModule],
|
||||||
|
providers: [CalDavClientProvider, CalDavGetEventsService],
|
||||||
|
exports: [CalDavGetEventsService],
|
||||||
|
})
|
||||||
|
export class CalDavDriverModule {}
|
||||||
@ -0,0 +1,493 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import * as ical from 'node-ical';
|
||||||
|
import {
|
||||||
|
calendarMultiGet,
|
||||||
|
createAccount,
|
||||||
|
DAVAccount,
|
||||||
|
DAVCalendar,
|
||||||
|
DAVNamespaceShort,
|
||||||
|
DAVObject,
|
||||||
|
fetchCalendars,
|
||||||
|
getBasicAuthHeaders,
|
||||||
|
syncCollection,
|
||||||
|
} from 'tsdav';
|
||||||
|
|
||||||
|
import { CalDavGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-get-events.service';
|
||||||
|
import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||||
|
import {
|
||||||
|
FetchedCalendarEvent,
|
||||||
|
FetchedCalendarEventParticipant,
|
||||||
|
} from 'src/modules/calendar/common/types/fetched-calendar-event';
|
||||||
|
|
||||||
|
const DEFAULT_CALENDAR_TYPE = 'caldav';
|
||||||
|
|
||||||
|
type CalendarCredentials = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
serverUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SimpleCalendar = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
isPrimary?: boolean;
|
||||||
|
syncToken?: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FetchEventsOptions = {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
syncCursor?: CalDAVSyncCursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CalDAVSyncResult = {
|
||||||
|
events: FetchedCalendarEvent[];
|
||||||
|
newSyncToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CalDAVSyncCursor = {
|
||||||
|
syncTokens: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CalDAVGetEventsResponse = {
|
||||||
|
events: FetchedCalendarEvent[];
|
||||||
|
syncCursor: CalDAVSyncCursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalDAVClient {
|
||||||
|
private credentials: CalendarCredentials;
|
||||||
|
private logger: Logger;
|
||||||
|
private headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(credentials: CalendarCredentials) {
|
||||||
|
this.credentials = credentials;
|
||||||
|
this.logger = new Logger(CalDAVClient.name);
|
||||||
|
this.headers = getBasicAuthHeaders({
|
||||||
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasFileExtension(url: string): boolean {
|
||||||
|
const fileName = url.substring(url.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
fileName.includes('.') &&
|
||||||
|
!fileName.substring(fileName.lastIndexOf('.')).includes('/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFileExtension(url: string): string {
|
||||||
|
if (!this.hasFileExtension(url)) return 'ics';
|
||||||
|
const fileName = url.substring(url.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
return fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isValidFormat(url: string): boolean {
|
||||||
|
const allowedExtensions = ['eml', 'ics'];
|
||||||
|
|
||||||
|
return allowedExtensions.includes(this.getFileExtension(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAccount(): Promise<DAVAccount> {
|
||||||
|
return createAccount({
|
||||||
|
account: {
|
||||||
|
serverUrl: this.credentials.serverUrl,
|
||||||
|
accountType: DEFAULT_CALENDAR_TYPE,
|
||||||
|
credentials: {
|
||||||
|
username: this.credentials.username,
|
||||||
|
password: this.credentials.password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listCalendars(): Promise<SimpleCalendar[]> {
|
||||||
|
try {
|
||||||
|
const account = await this.getAccount();
|
||||||
|
|
||||||
|
const calendars = (await fetchCalendars({
|
||||||
|
account,
|
||||||
|
headers: this.headers,
|
||||||
|
})) as (Omit<DAVCalendar, 'displayName'> & {
|
||||||
|
displayName?: string | Record<string, unknown>;
|
||||||
|
})[];
|
||||||
|
|
||||||
|
return calendars.reduce<SimpleCalendar[]>((result, calendar) => {
|
||||||
|
if (!calendar.components?.includes('VEVENT')) return result;
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
id: calendar.url,
|
||||||
|
url: calendar.url,
|
||||||
|
name:
|
||||||
|
typeof calendar.displayName === 'string'
|
||||||
|
? calendar.displayName
|
||||||
|
: 'Unnamed Calendar',
|
||||||
|
isPrimary: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error in ${CalDavGetEventsService.name} - getCalendarEvents`,
|
||||||
|
error.code,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an event is a full-day event by checking the raw iCal data.
|
||||||
|
* Full-day events use VALUE=DATE parameter in DTSTART/DTEND properties.
|
||||||
|
* Since node-ical converts all dates to JavaScript Date objects, we must check the raw data.
|
||||||
|
* @see https://tools.ietf.org/html/rfc5545#section-3.3.4 (DATE Value Type)
|
||||||
|
* @see https://tools.ietf.org/html/rfc5545#section-3.3.5 (DATE-TIME Value Type)
|
||||||
|
* @see https://tools.ietf.org/html/rfc5545#section-3.2.20 (VALUE Parameter)
|
||||||
|
*/
|
||||||
|
private isFullDayEvent(rawICalData: string): boolean {
|
||||||
|
const lines = rawICalData.split(/\r?\n/);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
if (
|
||||||
|
trimmedLine.startsWith('DTSTART') &&
|
||||||
|
trimmedLine.includes('VALUE=DATE')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractOrganizerFromEvent(
|
||||||
|
event: ical.VEvent,
|
||||||
|
): FetchedCalendarEventParticipant | null {
|
||||||
|
if (!event.organizer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizerEmail =
|
||||||
|
// @ts-expect-error - limitation of node-ical typing
|
||||||
|
event.organizer.val?.replace(/^mailto:/i, '') || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName:
|
||||||
|
// @ts-expect-error - limitation of node-ical typing
|
||||||
|
event.organizer.params?.CN || organizerEmail || 'Unknown',
|
||||||
|
responseStatus: CalendarEventParticipantResponseStatus.ACCEPTED,
|
||||||
|
handle: organizerEmail,
|
||||||
|
isOrganizer: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapPartStatToResponseStatus(
|
||||||
|
partStat: ical.AttendeePartStat,
|
||||||
|
): CalendarEventParticipantResponseStatus {
|
||||||
|
switch (partStat) {
|
||||||
|
case 'ACCEPTED':
|
||||||
|
return CalendarEventParticipantResponseStatus.ACCEPTED;
|
||||||
|
case 'DECLINED':
|
||||||
|
return CalendarEventParticipantResponseStatus.DECLINED;
|
||||||
|
case 'TENTATIVE':
|
||||||
|
return CalendarEventParticipantResponseStatus.TENTATIVE;
|
||||||
|
case 'NEEDS-ACTION':
|
||||||
|
default:
|
||||||
|
return CalendarEventParticipantResponseStatus.NEEDS_ACTION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractAttendeesFromEvent(
|
||||||
|
event: ical.VEvent,
|
||||||
|
): FetchedCalendarEventParticipant[] {
|
||||||
|
if (!event.attendee) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const attendees = Array.isArray(event.attendee)
|
||||||
|
? event.attendee
|
||||||
|
: [event.attendee];
|
||||||
|
|
||||||
|
return attendees.map((attendee: ical.Attendee) => {
|
||||||
|
// @ts-expect-error - limitation of node-ical typing
|
||||||
|
const handle = attendee.val?.replace(/^mailto:/i, '') || '';
|
||||||
|
// @ts-expect-error - limitation of node-ical typing
|
||||||
|
const displayName = attendee.params?.CN || handle || 'Unknown';
|
||||||
|
// @ts-expect-error - limitation of node-ical typing
|
||||||
|
const partStat = attendee.params?.PARTSTAT || 'NEEDS_ACTION';
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName,
|
||||||
|
responseStatus: this.mapPartStatToResponseStatus(partStat),
|
||||||
|
handle,
|
||||||
|
isOrganizer: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractParticipantsFromEvent(
|
||||||
|
event: ical.VEvent,
|
||||||
|
): FetchedCalendarEventParticipant[] {
|
||||||
|
const participants: FetchedCalendarEventParticipant[] = [];
|
||||||
|
|
||||||
|
const organizer = this.extractOrganizerFromEvent(event);
|
||||||
|
|
||||||
|
if (organizer) {
|
||||||
|
participants.push(organizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attendees = this.extractAttendeesFromEvent(event);
|
||||||
|
|
||||||
|
participants.push(...attendees);
|
||||||
|
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseICalData(
|
||||||
|
rawData: string,
|
||||||
|
objectUrl: string,
|
||||||
|
): FetchedCalendarEvent | null {
|
||||||
|
try {
|
||||||
|
const parsed = ical.parseICS(rawData);
|
||||||
|
const events = Object.values(parsed).filter(
|
||||||
|
(item) => item.type === 'VEVENT',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = events[0] as ical.VEvent;
|
||||||
|
const participants = this.extractParticipantsFromEvent(event);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: objectUrl,
|
||||||
|
title: event.summary || 'Untitled Event',
|
||||||
|
iCalUID: event.uid || '',
|
||||||
|
description: event.description || '',
|
||||||
|
startsAt: event.start.toISOString(),
|
||||||
|
endsAt: event.end.toISOString(),
|
||||||
|
location: event.location || '',
|
||||||
|
isFullDay: this.isFullDayEvent(rawData),
|
||||||
|
isCanceled: event.status === 'CANCELLED',
|
||||||
|
conferenceLinkLabel: '',
|
||||||
|
conferenceLinkUrl: event.url,
|
||||||
|
externalCreatedAt:
|
||||||
|
event.created?.toISOString() || new Date().toISOString(),
|
||||||
|
externalUpdatedAt:
|
||||||
|
event.lastmodified?.toISOString() ||
|
||||||
|
event.created?.toISOString() ||
|
||||||
|
new Date().toISOString(),
|
||||||
|
conferenceSolution: '',
|
||||||
|
recurringEventExternalId: event.recurrenceid
|
||||||
|
? String(event.recurrenceid)
|
||||||
|
: undefined,
|
||||||
|
participants,
|
||||||
|
status: event.status || 'CONFIRMED',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error in ${CalDavGetEventsService.name} - parseICalData`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvents(
|
||||||
|
options: FetchEventsOptions,
|
||||||
|
): Promise<CalDAVGetEventsResponse> {
|
||||||
|
const calendars = await this.listCalendars();
|
||||||
|
const results = new Map<string, CalDAVSyncResult>();
|
||||||
|
|
||||||
|
const syncPromises = calendars.map(async (calendar) => {
|
||||||
|
try {
|
||||||
|
const syncToken =
|
||||||
|
options.syncCursor?.syncTokens[calendar.url] ||
|
||||||
|
calendar.syncToken?.toString();
|
||||||
|
|
||||||
|
const syncResult = await syncCollection({
|
||||||
|
url: calendar.url,
|
||||||
|
props: {
|
||||||
|
[`${DAVNamespaceShort.DAV}:getetag`]: {},
|
||||||
|
[`${DAVNamespaceShort.CALDAV}:calendar-data`]: {},
|
||||||
|
},
|
||||||
|
syncLevel: 1,
|
||||||
|
...(syncToken ? { syncToken } : {}),
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allEvents: FetchedCalendarEvent[] = [];
|
||||||
|
|
||||||
|
const objectUrls = syncResult
|
||||||
|
.map((event) => event.href)
|
||||||
|
.filter((href): href is string => !!href && this.isValidFormat(href));
|
||||||
|
|
||||||
|
if (objectUrls.length > 0) {
|
||||||
|
try {
|
||||||
|
const calendarObjects = await calendarMultiGet({
|
||||||
|
url: calendar.url,
|
||||||
|
props: {
|
||||||
|
[`${DAVNamespaceShort.DAV}:getetag`]: {},
|
||||||
|
[`${DAVNamespaceShort.CALDAV}:calendar-data`]: {},
|
||||||
|
},
|
||||||
|
objectUrls: objectUrls,
|
||||||
|
depth: '1',
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const calendarObject of calendarObjects) {
|
||||||
|
if (calendarObject.props?.calendarData) {
|
||||||
|
const iCalData = this.extractICalData(
|
||||||
|
calendarObject.props?.calendarData,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!iCalData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = this.parseICalData(
|
||||||
|
iCalData,
|
||||||
|
calendarObject.href || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
event &&
|
||||||
|
this.isEventInTimeRange(
|
||||||
|
{
|
||||||
|
url: calendarObject.href || '',
|
||||||
|
data: calendarObject.props.calendarData,
|
||||||
|
etag: calendarObject.props.getetag,
|
||||||
|
},
|
||||||
|
options.startDate,
|
||||||
|
options.endDate,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
allEvents.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (fetchError) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error in ${CalDavGetEventsService.name} - getEvents`,
|
||||||
|
fetchError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let newSyncToken = syncToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const account = await this.getAccount();
|
||||||
|
const updatedCalendars = await fetchCalendars({
|
||||||
|
account,
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
const updatedCalendar = updatedCalendars.find(
|
||||||
|
(cal) => cal.url === calendar.url,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedCalendar?.syncToken) {
|
||||||
|
newSyncToken = updatedCalendar.syncToken.toString();
|
||||||
|
}
|
||||||
|
} catch (syncTokenError) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error in ${CalDavGetEventsService.name} - getEvents`,
|
||||||
|
syncTokenError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.set(calendar.url, {
|
||||||
|
events: allEvents,
|
||||||
|
newSyncToken,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
results.set(calendar.url, {
|
||||||
|
events: [],
|
||||||
|
newSyncToken: options.syncCursor?.syncTokens[calendar.url],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(syncPromises);
|
||||||
|
|
||||||
|
const allEvents = Array.from(results.values())
|
||||||
|
.map((result) => result.events)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
const syncTokens: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [calendarUrl, result] of results) {
|
||||||
|
if (result.newSyncToken) {
|
||||||
|
syncTokens[calendarUrl] = result.newSyncToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: allEvents,
|
||||||
|
syncCursor: { syncTokens },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts iCal data from various CalDAV server response formats.
|
||||||
|
* Some servers return data directly as a string, others nest it under _cdata or some other properties.
|
||||||
|
*/
|
||||||
|
private extractICalData(
|
||||||
|
calendarData: string | Record<string, unknown>,
|
||||||
|
): string | null {
|
||||||
|
if (!calendarData) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof calendarData === 'string' &&
|
||||||
|
calendarData.includes('VCALENDAR')
|
||||||
|
) {
|
||||||
|
return calendarData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof calendarData === 'object' && calendarData !== null) {
|
||||||
|
for (const key in calendarData) {
|
||||||
|
const result = this.extractICalData(
|
||||||
|
calendarData[key] as string | Record<string, unknown>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isEventInTimeRange(
|
||||||
|
davObject: DAVObject,
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date,
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
if (!davObject.data) return false;
|
||||||
|
|
||||||
|
const parsed = ical.parseICS(davObject.data);
|
||||||
|
const events = Object.values(parsed).filter(
|
||||||
|
(item) => item.type === 'VEVENT',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (events.length === 0) return false;
|
||||||
|
|
||||||
|
const event = events[0] as ical.VEvent;
|
||||||
|
|
||||||
|
return event.start < endDate && event.end > startDate;
|
||||||
|
} catch (error) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
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()
|
||||||
|
export class CalDavClientProvider {
|
||||||
|
public async getCalDavCalendarClient(
|
||||||
|
connectedAccount: Pick<
|
||||||
|
ConnectedAccountWorkspaceEntity,
|
||||||
|
'id' | 'provider' | 'connectionParameters' | 'handle'
|
||||||
|
>,
|
||||||
|
): Promise<CalDAVClient> {
|
||||||
|
if (
|
||||||
|
!connectedAccount.connectionParameters?.CALDAV?.password ||
|
||||||
|
!connectedAccount.connectionParameters?.CALDAV?.host
|
||||||
|
) {
|
||||||
|
throw new Error('Missing required CalDAV connection parameters');
|
||||||
|
}
|
||||||
|
const caldavClient = new CalDAVClient({
|
||||||
|
username:
|
||||||
|
connectedAccount.connectionParameters.CALDAV.username ??
|
||||||
|
connectedAccount.handle,
|
||||||
|
password: connectedAccount.connectionParameters.CALDAV.password,
|
||||||
|
serverUrl: connectedAccount.connectionParameters.CALDAV.host,
|
||||||
|
});
|
||||||
|
|
||||||
|
return caldavClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { CalDavClientProvider } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/providers/caldav.provider';
|
||||||
|
import { parseCalDAVError } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/utils/parse-caldav-error.util';
|
||||||
|
import { GetCalendarEventsResponse } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalDavGetEventsService {
|
||||||
|
private readonly logger = new Logger(CalDavGetEventsService.name);
|
||||||
|
|
||||||
|
private static readonly PAST_DAYS_WINDOW = 365 * 5;
|
||||||
|
private static readonly FUTURE_DAYS_WINDOW = 365;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly caldavCalendarClientProvider: CalDavClientProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async getCalendarEvents(
|
||||||
|
connectedAccount: Pick<
|
||||||
|
ConnectedAccountWorkspaceEntity,
|
||||||
|
'provider' | 'id' | 'connectionParameters' | 'handle'
|
||||||
|
>,
|
||||||
|
syncCursor?: string,
|
||||||
|
): Promise<GetCalendarEventsResponse> {
|
||||||
|
this.logger.log(`Getting calendar events for ${connectedAccount.handle}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const caldavCalendarClient =
|
||||||
|
await this.caldavCalendarClientProvider.getCalDavCalendarClient(
|
||||||
|
connectedAccount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const startDate = new Date(
|
||||||
|
Date.now() -
|
||||||
|
CalDavGetEventsService.PAST_DAYS_WINDOW * 24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
const endDate = new Date(
|
||||||
|
Date.now() +
|
||||||
|
CalDavGetEventsService.FUTURE_DAYS_WINDOW * 24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await caldavCalendarClient.getEvents({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
syncCursor: syncCursor ? JSON.parse(syncCursor) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Found ${result.events.length} calendar events for ${connectedAccount.handle}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullEvents: true,
|
||||||
|
calendarEvents: result.events,
|
||||||
|
nextSyncCursor: JSON.stringify(result.syncCursor),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(error as Error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: Error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error in ${CalDavGetEventsService.name} - getCalendarEvents`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw parseCalDAVError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
CalendarEventImportDriverException,
|
||||||
|
CalendarEventImportDriverExceptionCode,
|
||||||
|
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
|
||||||
|
|
||||||
|
export const parseCalDAVError = (
|
||||||
|
error: Error,
|
||||||
|
): CalendarEventImportDriverException => {
|
||||||
|
const { message } = error;
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case 'Collection does not exist on server':
|
||||||
|
return new CalendarEventImportDriverException(
|
||||||
|
message,
|
||||||
|
CalendarEventImportDriverExceptionCode.NOT_FOUND,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'no account for smartCollectionSync':
|
||||||
|
case 'no account for fetchAddressBooks':
|
||||||
|
case 'no account for fetchCalendars':
|
||||||
|
case 'Must have account before syncCalendars':
|
||||||
|
return new CalendarEventImportDriverException(
|
||||||
|
message,
|
||||||
|
CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'cannot fetchVCards for undefined addressBook':
|
||||||
|
case 'cannot find calendarUserAddresses':
|
||||||
|
case 'cannot fetchCalendarObjects for undefined calendar':
|
||||||
|
case 'cannot find homeUrl':
|
||||||
|
return new CalendarEventImportDriverException(
|
||||||
|
message,
|
||||||
|
CalendarEventImportDriverExceptionCode.NOT_FOUND,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'Invalid credentials':
|
||||||
|
return new CalendarEventImportDriverException(
|
||||||
|
message,
|
||||||
|
CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'Invalid auth method':
|
||||||
|
return new CalendarEventImportDriverException(
|
||||||
|
message,
|
||||||
|
CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CalendarEventImportDriverException(
|
||||||
|
message,
|
||||||
|
CalendarEventImportDriverExceptionCode.UNKNOWN,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { CalDavGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-get-events.service';
|
||||||
import { GoogleCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service';
|
import { GoogleCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/services/google-calendar-get-events.service';
|
||||||
import { MicrosoftCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service';
|
import { MicrosoftCalendarGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-get-events.service';
|
||||||
import {
|
import {
|
||||||
@ -23,12 +24,13 @@ export class CalendarGetCalendarEventsService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly googleCalendarGetEventsService: GoogleCalendarGetEventsService,
|
private readonly googleCalendarGetEventsService: GoogleCalendarGetEventsService,
|
||||||
private readonly microsoftCalendarGetEventsService: MicrosoftCalendarGetEventsService,
|
private readonly microsoftCalendarGetEventsService: MicrosoftCalendarGetEventsService,
|
||||||
|
private readonly caldavCalendarGetEventsService: CalDavGetEventsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getCalendarEvents(
|
public async getCalendarEvents(
|
||||||
connectedAccount: Pick<
|
connectedAccount: Pick<
|
||||||
ConnectedAccountWorkspaceEntity,
|
ConnectedAccountWorkspaceEntity,
|
||||||
'provider' | 'refreshToken' | 'id'
|
'provider' | 'refreshToken' | 'id' | 'connectionParameters' | 'handle'
|
||||||
>,
|
>,
|
||||||
syncCursor?: string,
|
syncCursor?: string,
|
||||||
): Promise<GetCalendarEventsResponse> {
|
): Promise<GetCalendarEventsResponse> {
|
||||||
@ -43,6 +45,11 @@ export class CalendarGetCalendarEventsService {
|
|||||||
connectedAccount,
|
connectedAccount,
|
||||||
syncCursor,
|
syncCursor,
|
||||||
);
|
);
|
||||||
|
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
|
||||||
|
return this.caldavCalendarGetEventsService.getCalendarEvents(
|
||||||
|
connectedAccount,
|
||||||
|
syncCursor,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new CalendarEventImportException(
|
throw new CalendarEventImportException(
|
||||||
`Provider ${connectedAccount.provider} is not supported`,
|
`Provider ${connectedAccount.provider} is not supported`,
|
||||||
|
|||||||
@ -2,22 +2,26 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
import { EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto';
|
||||||
import {
|
|
||||||
AccountType,
|
|
||||||
ConnectionParameters,
|
|
||||||
} from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
|
||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||||
|
import {
|
||||||
|
CalendarEventListFetchJob,
|
||||||
|
CalendarEventListFetchJobData,
|
||||||
|
} from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
|
||||||
|
import {
|
||||||
|
CalendarChannelSyncStage,
|
||||||
|
CalendarChannelSyncStatus,
|
||||||
|
CalendarChannelWorkspaceEntity,
|
||||||
|
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
import {
|
import {
|
||||||
MessageChannelSyncStage,
|
MessageChannelSyncStage,
|
||||||
@ -36,28 +40,22 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
@InjectMessageQueue(MessageQueue.messagingQueue)
|
@InjectMessageQueue(MessageQueue.messagingQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
private readonly twentyConfigService: TwentyConfigService,
|
@InjectMessageQueue(MessageQueue.calendarQueue)
|
||||||
|
private readonly calendarQueueService: MessageQueueService,
|
||||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||||
@InjectRepository(ObjectMetadataEntity, 'core')
|
@InjectRepository(ObjectMetadataEntity, 'core')
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
private readonly objectMetadataRepository: WorkspaceRepository<ObjectMetadataEntity>,
|
||||||
private readonly featureFlagService: FeatureFlagService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async setupConnectedAccount(input: {
|
async setupCompleteAccount(input: {
|
||||||
handle: string;
|
handle: string;
|
||||||
workspaceMemberId: string;
|
workspaceMemberId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
accountType: AccountType;
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
connectionParams: ConnectionParameters;
|
|
||||||
connectedAccountId?: string;
|
connectedAccountId?: string;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const { handle, workspaceId, workspaceMemberId, connectedAccountId } =
|
||||||
handle,
|
input;
|
||||||
workspaceId,
|
|
||||||
workspaceMemberId,
|
|
||||||
connectionParams,
|
|
||||||
connectedAccountId,
|
|
||||||
} = input;
|
|
||||||
|
|
||||||
const connectedAccountRepository =
|
const connectedAccountRepository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
|
||||||
@ -65,7 +63,19 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
'connectedAccount',
|
'connectedAccount',
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectedAccount = connectedAccountId
|
const messageChannelRepository =
|
||||||
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
|
||||||
|
workspaceId,
|
||||||
|
'messageChannel',
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarChannelRepository =
|
||||||
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<CalendarChannelWorkspaceEntity>(
|
||||||
|
workspaceId,
|
||||||
|
'calendarChannel',
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingAccount = connectedAccountId
|
||||||
? await connectedAccountRepository.findOne({
|
? await connectedAccountRepository.findOne({
|
||||||
where: { id: connectedAccountId },
|
where: { id: connectedAccountId },
|
||||||
})
|
})
|
||||||
@ -73,191 +83,251 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
where: { handle, accountOwnerId: workspaceMemberId },
|
where: { handle, accountOwnerId: workspaceMemberId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingAccountId = connectedAccount?.id;
|
const accountId = existingAccount?.id ?? connectedAccountId ?? v4();
|
||||||
const newOrExistingConnectedAccountId =
|
|
||||||
existingAccountId ?? connectedAccountId ?? v4();
|
|
||||||
|
|
||||||
const messageChannelRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'messageChannel',
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceDataSource =
|
const workspaceDataSource =
|
||||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
let shouldEnableSync = false;
|
let createdMessageChannel: MessageChannelWorkspaceEntity | null = null;
|
||||||
|
let createdCalendarChannel: CalendarChannelWorkspaceEntity | null = null;
|
||||||
if (connectedAccount) {
|
|
||||||
const hadOnlySmtp =
|
|
||||||
connectedAccount.connectionParameters?.SMTP &&
|
|
||||||
!connectedAccount.connectionParameters?.IMAP &&
|
|
||||||
!connectedAccount.connectionParameters?.CALDAV;
|
|
||||||
|
|
||||||
const isAddingImapOrCaldav =
|
|
||||||
input.accountType === 'IMAP' || input.accountType === 'CALDAV';
|
|
||||||
|
|
||||||
shouldEnableSync = Boolean(hadOnlySmtp && isAddingImapOrCaldav);
|
|
||||||
}
|
|
||||||
|
|
||||||
await workspaceDataSource.transaction(async () => {
|
await workspaceDataSource.transaction(async () => {
|
||||||
if (!existingAccountId) {
|
await this.upsertConnectedAccount(
|
||||||
const newConnectedAccount = await connectedAccountRepository.save(
|
input,
|
||||||
{
|
accountId,
|
||||||
id: newOrExistingConnectedAccountId,
|
existingAccount,
|
||||||
handle,
|
connectedAccountRepository,
|
||||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
);
|
||||||
connectionParameters: {
|
|
||||||
[input.accountType]: connectionParams,
|
|
||||||
},
|
|
||||||
accountOwnerId: workspaceMemberId,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectedAccountMetadata =
|
createdMessageChannel = await this.setupMessageChannels(
|
||||||
await this.objectMetadataRepository.findOneOrFail({
|
input,
|
||||||
where: { nameSingular: 'connectedAccount', workspaceId },
|
accountId,
|
||||||
});
|
messageChannelRepository,
|
||||||
|
);
|
||||||
|
|
||||||
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
createdCalendarChannel = await this.setupCalendarChannels(
|
||||||
objectMetadataNameSingular: 'connectedAccount',
|
input,
|
||||||
action: DatabaseEventAction.CREATED,
|
accountId,
|
||||||
events: [
|
calendarChannelRepository,
|
||||||
{
|
);
|
||||||
recordId: newConnectedAccount.id,
|
|
||||||
objectMetadata: connectedAccountMetadata,
|
|
||||||
properties: {
|
|
||||||
after: newConnectedAccount,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newMessageChannel = await messageChannelRepository.save(
|
|
||||||
{
|
|
||||||
id: v4(),
|
|
||||||
connectedAccountId: newOrExistingConnectedAccountId,
|
|
||||||
type: MessageChannelType.EMAIL,
|
|
||||||
handle,
|
|
||||||
isSyncEnabled: shouldEnableSync,
|
|
||||||
syncStatus: shouldEnableSync
|
|
||||||
? MessageChannelSyncStatus.ONGOING
|
|
||||||
: MessageChannelSyncStatus.NOT_SYNCED,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const messageChannelMetadata =
|
|
||||||
await this.objectMetadataRepository.findOneOrFail({
|
|
||||||
where: { nameSingular: 'messageChannel', workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
|
||||||
objectMetadataNameSingular: 'messageChannel',
|
|
||||||
action: DatabaseEventAction.CREATED,
|
|
||||||
events: [
|
|
||||||
{
|
|
||||||
recordId: newMessageChannel.id,
|
|
||||||
objectMetadata: messageChannelMetadata,
|
|
||||||
properties: {
|
|
||||||
after: newMessageChannel,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const updatedConnectedAccount = await connectedAccountRepository.update(
|
|
||||||
{
|
|
||||||
id: newOrExistingConnectedAccountId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
connectionParameters: {
|
|
||||||
...connectedAccount.connectionParameters,
|
|
||||||
[input.accountType]: connectionParams,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectedAccountMetadata =
|
|
||||||
await this.objectMetadataRepository.findOneOrFail({
|
|
||||||
where: { nameSingular: 'connectedAccount', workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
|
||||||
objectMetadataNameSingular: 'connectedAccount',
|
|
||||||
action: DatabaseEventAction.UPDATED,
|
|
||||||
events: [
|
|
||||||
{
|
|
||||||
recordId: newOrExistingConnectedAccountId,
|
|
||||||
objectMetadata: connectedAccountMetadata,
|
|
||||||
properties: {
|
|
||||||
before: connectedAccount,
|
|
||||||
after: {
|
|
||||||
...connectedAccount,
|
|
||||||
...updatedConnectedAccount.raw[0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageChannels = await messageChannelRepository.find({
|
|
||||||
where: { connectedAccountId: newOrExistingConnectedAccountId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageChannelUpdates = await messageChannelRepository.update(
|
|
||||||
{
|
|
||||||
connectedAccountId: newOrExistingConnectedAccountId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
syncStage: MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING,
|
|
||||||
syncStatus: shouldEnableSync
|
|
||||||
? MessageChannelSyncStatus.ONGOING
|
|
||||||
: MessageChannelSyncStatus.NOT_SYNCED,
|
|
||||||
syncCursor: '',
|
|
||||||
syncStageStartedAt: null,
|
|
||||||
isSyncEnabled: shouldEnableSync,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const messageChannelMetadata =
|
|
||||||
await this.objectMetadataRepository.findOneOrFail({
|
|
||||||
where: { nameSingular: 'messageChannel', workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
|
||||||
objectMetadataNameSingular: 'messageChannel',
|
|
||||||
action: DatabaseEventAction.UPDATED,
|
|
||||||
events: messageChannels.map((messageChannel) => ({
|
|
||||||
recordId: messageChannel.id,
|
|
||||||
objectMetadata: messageChannelMetadata,
|
|
||||||
properties: {
|
|
||||||
before: messageChannel,
|
|
||||||
after: { ...messageChannel, ...messageChannelUpdates.raw[0] },
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!shouldEnableSync) {
|
await this.enqueueSyncJobs(
|
||||||
return;
|
input,
|
||||||
|
accountId,
|
||||||
|
workspaceId,
|
||||||
|
createdMessageChannel,
|
||||||
|
createdCalendarChannel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async upsertConnectedAccount(
|
||||||
|
input: {
|
||||||
|
handle: string;
|
||||||
|
workspaceMemberId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
|
},
|
||||||
|
accountId: string,
|
||||||
|
existingAccount: ConnectedAccountWorkspaceEntity | null,
|
||||||
|
connectedAccountRepository: WorkspaceRepository<ConnectedAccountWorkspaceEntity>,
|
||||||
|
) {
|
||||||
|
const accountData = {
|
||||||
|
id: accountId,
|
||||||
|
handle: input.handle,
|
||||||
|
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||||
|
connectionParameters: input.connectionParameters,
|
||||||
|
accountOwnerId: input.workspaceMemberId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const savedAccount = await connectedAccountRepository.save(accountData, {});
|
||||||
|
|
||||||
|
const connectedAccountMetadata =
|
||||||
|
await this.objectMetadataRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
nameSingular: 'connectedAccount',
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingAccount) {
|
||||||
|
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
||||||
|
objectMetadataNameSingular: 'connectedAccount',
|
||||||
|
action: DatabaseEventAction.UPDATED,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
recordId: savedAccount.id,
|
||||||
|
objectMetadata: connectedAccountMetadata,
|
||||||
|
properties: {
|
||||||
|
before: existingAccount,
|
||||||
|
after: savedAccount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
||||||
|
objectMetadataNameSingular: 'connectedAccount',
|
||||||
|
action: DatabaseEventAction.CREATED,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
recordId: savedAccount.id,
|
||||||
|
objectMetadata: connectedAccountMetadata,
|
||||||
|
properties: {
|
||||||
|
after: savedAccount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupMessageChannels(
|
||||||
|
input: {
|
||||||
|
handle: string;
|
||||||
|
workspaceId: string;
|
||||||
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
|
},
|
||||||
|
accountId: string,
|
||||||
|
messageChannelRepository: WorkspaceRepository<MessageChannelWorkspaceEntity>,
|
||||||
|
): Promise<MessageChannelWorkspaceEntity | null> {
|
||||||
|
const existingChannels = await messageChannelRepository.find({
|
||||||
|
where: { connectedAccountId: accountId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingChannels.length > 0) {
|
||||||
|
await messageChannelRepository.delete({
|
||||||
|
connectedAccountId: accountId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageChannels = await messageChannelRepository.find({
|
const shouldEnableSync = Boolean(input.connectionParameters.IMAP);
|
||||||
where: {
|
|
||||||
connectedAccountId: newOrExistingConnectedAccountId,
|
const newMessageChannel = await messageChannelRepository.save(
|
||||||
|
{
|
||||||
|
id: v4(),
|
||||||
|
connectedAccountId: accountId,
|
||||||
|
type: MessageChannelType.EMAIL,
|
||||||
|
handle: input.handle,
|
||||||
|
isSyncEnabled: shouldEnableSync,
|
||||||
|
syncStatus: shouldEnableSync
|
||||||
|
? MessageChannelSyncStatus.ONGOING
|
||||||
|
: MessageChannelSyncStatus.NOT_SYNCED,
|
||||||
|
syncStage: shouldEnableSync
|
||||||
|
? MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING
|
||||||
|
: undefined,
|
||||||
|
syncCursor: '',
|
||||||
|
syncStageStartedAt: null,
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageChannelMetadata =
|
||||||
|
await this.objectMetadataRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
nameSingular: 'messageChannel',
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
||||||
|
objectMetadataNameSingular: 'messageChannel',
|
||||||
|
action: DatabaseEventAction.CREATED,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
recordId: newMessageChannel.id,
|
||||||
|
objectMetadata: messageChannelMetadata,
|
||||||
|
properties: {
|
||||||
|
after: newMessageChannel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const messageChannel of messageChannels) {
|
return shouldEnableSync ? newMessageChannel : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupCalendarChannels(
|
||||||
|
input: {
|
||||||
|
handle: string;
|
||||||
|
workspaceId: string;
|
||||||
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
|
},
|
||||||
|
accountId: string,
|
||||||
|
calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
|
||||||
|
): Promise<CalendarChannelWorkspaceEntity | null> {
|
||||||
|
const existingChannels = await calendarChannelRepository.find({
|
||||||
|
where: { connectedAccountId: accountId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingChannels.length > 0) {
|
||||||
|
await calendarChannelRepository.delete({
|
||||||
|
connectedAccountId: accountId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldEnableSync = Boolean(input.connectionParameters.CALDAV);
|
||||||
|
|
||||||
|
if (shouldEnableSync) {
|
||||||
|
const newCalendarChannel = await calendarChannelRepository.save(
|
||||||
|
{
|
||||||
|
id: v4(),
|
||||||
|
connectedAccountId: accountId,
|
||||||
|
handle: input.handle,
|
||||||
|
isSyncEnabled: shouldEnableSync,
|
||||||
|
syncStatus: CalendarChannelSyncStatus.ONGOING,
|
||||||
|
syncStage:
|
||||||
|
CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING,
|
||||||
|
syncCursor: '',
|
||||||
|
syncStageStartedAt: null,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarChannelMetadata =
|
||||||
|
await this.objectMetadataRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
nameSingular: 'calendarChannel',
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.workspaceEventEmitter.emitDatabaseBatchEvent({
|
||||||
|
objectMetadataNameSingular: 'calendarChannel',
|
||||||
|
action: DatabaseEventAction.CREATED,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
recordId: newCalendarChannel.id,
|
||||||
|
objectMetadata: calendarChannelMetadata,
|
||||||
|
properties: {
|
||||||
|
after: newCalendarChannel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspaceId: input.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newCalendarChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async enqueueSyncJobs(
|
||||||
|
input: {
|
||||||
|
connectionParameters: EmailAccountConnectionParameters;
|
||||||
|
},
|
||||||
|
accountId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
messageChannel: MessageChannelWorkspaceEntity | null,
|
||||||
|
calendarChannel: CalendarChannelWorkspaceEntity | null,
|
||||||
|
) {
|
||||||
|
if (input.connectionParameters.IMAP && messageChannel) {
|
||||||
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
||||||
MessagingMessageListFetchJob.name,
|
MessagingMessageListFetchJob.name,
|
||||||
{
|
{
|
||||||
@ -266,5 +336,15 @@ export class ImapSmtpCalDavAPIService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.connectionParameters.CALDAV && calendarChannel) {
|
||||||
|
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
|
||||||
|
CalendarEventListFetchJob.name,
|
||||||
|
{
|
||||||
|
workspaceId,
|
||||||
|
calendarChannelId: calendarChannel.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
89
yarn.lock
89
yarn.lock
@ -27625,7 +27625,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"axios@npm:^1.6.2":
|
"axios@npm:^1.6.2, axios@npm:^1.7.7":
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
resolution: "axios@npm:1.10.0"
|
resolution: "axios@npm:1.10.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -28826,6 +28826,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"base-64@npm:1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "base-64@npm:1.0.0"
|
||||||
|
checksum: 10c0/d886cb3236cee0bed9f7075675748b59b32fad623ddb8ce1793c790306aa0f76a03238cad4b3fb398abda6527ce08a5588388533a4ccade0b97e82b9da660e28
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"base64-js@npm:0.0.8":
|
"base64-js@npm:0.0.8":
|
||||||
version: 0.0.8
|
version: 0.0.8
|
||||||
resolution: "base64-js@npm:0.0.8"
|
resolution: "base64-js@npm:0.0.8"
|
||||||
@ -31760,6 +31767,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cross-fetch@npm:4.1.0":
|
||||||
|
version: 4.1.0
|
||||||
|
resolution: "cross-fetch@npm:4.1.0"
|
||||||
|
dependencies:
|
||||||
|
node-fetch: "npm:^2.7.0"
|
||||||
|
checksum: 10c0/628b134ea27cfcada67025afe6ef1419813fffc5d63d175553efa75a2334522d450300a0f3f0719029700da80e96327930709d5551cf6deb39bb62f1d536642e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"cross-fetch@npm:^3.0.4, cross-fetch@npm:^3.1.5":
|
"cross-fetch@npm:^3.0.4, cross-fetch@npm:^3.1.5":
|
||||||
version: 3.1.8
|
version: 3.1.8
|
||||||
resolution: "cross-fetch@npm:3.1.8"
|
resolution: "cross-fetch@npm:3.1.8"
|
||||||
@ -32577,6 +32593,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"debug@npm:4.4.1":
|
||||||
|
version: 4.4.1
|
||||||
|
resolution: "debug@npm:4.4.1"
|
||||||
|
dependencies:
|
||||||
|
ms: "npm:^2.1.3"
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"debug@npm:^3.2.7":
|
"debug@npm:^3.2.7":
|
||||||
version: 3.2.7
|
version: 3.2.7
|
||||||
resolution: "debug@npm:3.2.7"
|
resolution: "debug@npm:3.2.7"
|
||||||
@ -46345,7 +46373,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"moment@npm:^2.22.1":
|
"moment-timezone@npm:^0.5.45":
|
||||||
|
version: 0.5.48
|
||||||
|
resolution: "moment-timezone@npm:0.5.48"
|
||||||
|
dependencies:
|
||||||
|
moment: "npm:^2.29.4"
|
||||||
|
checksum: 10c0/ab14ec9d94bc33f29ac18e5417b7f8aca0b17130b952c5cc9697b8fea839e5ece9313af5fd3c9703a05db472b1560ddbfc7ad2aa24aac9afd047d6da6c3c6033
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"moment@npm:^2.22.1, moment@npm:^2.29.4":
|
||||||
version: 2.30.1
|
version: 2.30.1
|
||||||
resolution: "moment@npm:2.30.1"
|
resolution: "moment@npm:2.30.1"
|
||||||
checksum: 10c0/865e4279418c6de666fca7786607705fd0189d8a7b7624e2e56be99290ac846f90878a6f602e34b4e0455c549b85385b1baf9966845962b313699e7cb847543a
|
checksum: 10c0/865e4279418c6de666fca7786607705fd0189d8a7b7624e2e56be99290ac846f90878a6f602e34b4e0455c549b85385b1baf9966845962b313699e7cb847543a
|
||||||
@ -47093,7 +47130,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"node-fetch@npm:^2, node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9":
|
"node-fetch@npm:^2, node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0":
|
||||||
version: 2.7.0
|
version: 2.7.0
|
||||||
resolution: "node-fetch@npm:2.7.0"
|
resolution: "node-fetch@npm:2.7.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -47199,6 +47236,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"node-ical@npm:^0.20.1":
|
||||||
|
version: 0.20.1
|
||||||
|
resolution: "node-ical@npm:0.20.1"
|
||||||
|
dependencies:
|
||||||
|
axios: "npm:^1.7.7"
|
||||||
|
moment-timezone: "npm:^0.5.45"
|
||||||
|
rrule: "npm:2.8.1"
|
||||||
|
uuid: "npm:^10.0.0"
|
||||||
|
checksum: 10c0/3e95ece63f0420f96b611913e0134660120bf2e87da8b1afbc73b6db2cbe2ad20d9b76ad912fa80b20923c64f0002869d98edaa53d143aa5989d223ff12ef621
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"node-int64@npm:^0.4.0":
|
"node-int64@npm:^0.4.0":
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
resolution: "node-int64@npm:0.4.0"
|
resolution: "node-int64@npm:0.4.0"
|
||||||
@ -53194,6 +53243,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"rrule@npm:2.8.1":
|
||||||
|
version: 2.8.1
|
||||||
|
resolution: "rrule@npm:2.8.1"
|
||||||
|
dependencies:
|
||||||
|
tslib: "npm:^2.4.0"
|
||||||
|
checksum: 10c0/c9350620bbd57d0cdf99320b576121a2d7ed579fda4ae50891e2779c7dfd6dc7b174b558b6598adefb1f0e053549dc977740e1a265f77dcf0827aaeea60b45e7
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"rrweb-cssom@npm:^0.6.0":
|
"rrweb-cssom@npm:^0.6.0":
|
||||||
version: 0.6.0
|
version: 0.6.0
|
||||||
resolution: "rrweb-cssom@npm:0.6.0"
|
resolution: "rrweb-cssom@npm:0.6.0"
|
||||||
@ -56654,6 +56712,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tsdav@npm:^2.1.5":
|
||||||
|
version: 2.1.5
|
||||||
|
resolution: "tsdav@npm:2.1.5"
|
||||||
|
dependencies:
|
||||||
|
base-64: "npm:1.0.0"
|
||||||
|
cross-fetch: "npm:4.1.0"
|
||||||
|
debug: "npm:4.4.1"
|
||||||
|
xml-js: "npm:1.6.11"
|
||||||
|
checksum: 10c0/ff04bb9b61413c07d4532926f9d4a8b6788af77c6e3aa87813c797a06e59348398d2d9ecb15ef3e82e4ae013fe764c73511c3bbfaff5fb6c4484f5f15099499f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tslib@npm:2, tslib@npm:^2.8.0":
|
"tslib@npm:2, tslib@npm:^2.8.0":
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
resolution: "tslib@npm:2.8.1"
|
resolution: "tslib@npm:2.8.1"
|
||||||
@ -57002,6 +57072,7 @@ __metadata:
|
|||||||
lodash.uniqby: "npm:^4.7.0"
|
lodash.uniqby: "npm:^4.7.0"
|
||||||
monaco-editor: "npm:^0.51.0"
|
monaco-editor: "npm:^0.51.0"
|
||||||
monaco-editor-auto-typings: "npm:^0.4.5"
|
monaco-editor-auto-typings: "npm:^0.4.5"
|
||||||
|
node-ical: "npm:^0.20.1"
|
||||||
openid-client: "npm:^5.7.0"
|
openid-client: "npm:^5.7.0"
|
||||||
otplib: "npm:^12.0.1"
|
otplib: "npm:^12.0.1"
|
||||||
passport: "npm:^0.7.0"
|
passport: "npm:^0.7.0"
|
||||||
@ -57010,6 +57081,7 @@ __metadata:
|
|||||||
rimraf: "npm:^5.0.5"
|
rimraf: "npm:^5.0.5"
|
||||||
ts-morph: "npm:^24.0.0"
|
ts-morph: "npm:^24.0.0"
|
||||||
tsconfig-paths: "npm:^4.2.0"
|
tsconfig-paths: "npm:^4.2.0"
|
||||||
|
tsdav: "npm:^2.1.5"
|
||||||
twenty-emails: "workspace:*"
|
twenty-emails: "workspace:*"
|
||||||
twenty-shared: "workspace:*"
|
twenty-shared: "workspace:*"
|
||||||
typeorm: "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch"
|
typeorm: "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch"
|
||||||
@ -58805,6 +58877,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"uuid@npm:^10.0.0":
|
||||||
|
version: 10.0.0
|
||||||
|
resolution: "uuid@npm:10.0.0"
|
||||||
|
bin:
|
||||||
|
uuid: dist/bin/uuid
|
||||||
|
checksum: 10c0/eab18c27fe4ab9fb9709a5d5f40119b45f2ec8314f8d4cf12ce27e4c6f4ffa4a6321dc7db6c515068fa373c075b49691ba969f0010bf37f44c37ca40cd6bf7fe
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"uuid@npm:^3.3.2":
|
"uuid@npm:^3.3.2":
|
||||||
version: 3.4.0
|
version: 3.4.0
|
||||||
resolution: "uuid@npm:3.4.0"
|
resolution: "uuid@npm:3.4.0"
|
||||||
@ -60309,7 +60390,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"xml-js@npm:^1.6.8":
|
"xml-js@npm:1.6.11, xml-js@npm:^1.6.8":
|
||||||
version: 1.6.11
|
version: 1.6.11
|
||||||
resolution: "xml-js@npm:1.6.11"
|
resolution: "xml-js@npm:1.6.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user