diff --git a/src/frontend/apps/calendars/src/features/calendar/services/dav/caldav-helpers.ts b/src/frontend/apps/calendars/src/features/calendar/services/dav/caldav-helpers.ts
new file mode 100644
index 0000000..f6215a3
--- /dev/null
+++ b/src/frontend/apps/calendars/src/features/calendar/services/dav/caldav-helpers.ts
@@ -0,0 +1,418 @@
+/**
+ * CalDAV Helper Functions
+ *
+ * Factorized utilities for XML building, DAV requests, and error handling.
+ */
+
+import { davRequest, propfind, DAVNamespaceShort } from 'tsdav'
+import type { DAVMethods } from 'tsdav'
+
+type HTTPMethods = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'
+type AllowedMethods = DAVMethods | HTTPMethods
+import type { SharePrivilege, CalDavResponse } from './types/caldav-service'
+
+// ============================================================================
+// XML Helpers
+// ============================================================================
+
+/** Escape special XML characters */
+export function escapeXml(str: string | undefined | null): string {
+ if (str === undefined || str === null) {
+ return '';
+ }
+ if (typeof str !== 'string') {
+ str = String(str);
+ }
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+}
+
+/** XML namespaces used in CalDAV */
+export const XML_NS = {
+ DAV: 'xmlns:D="DAV:"',
+ CALDAV: 'xmlns:C="urn:ietf:params:xml:ns:caldav"',
+ APPLE: 'xmlns:A="http://apple.com/ns/ical/"',
+ CS: 'xmlns:CS="http://calendarserver.org/ns/"',
+} as const
+
+/** Build XML prop element */
+export function xmlProp(namespace: string, name: string, value: string): string {
+ return `<${namespace}:${name}>${escapeXml(value)}${namespace}:${name}>`
+}
+
+/** Build XML with optional value (returns empty string if value is undefined) */
+export function xmlPropOptional(namespace: string, name: string, value: string | undefined): string {
+ return value !== undefined ? xmlProp(namespace, name, value) : ''
+}
+
+// ============================================================================
+// Calendar Property Builders
+// ============================================================================
+
+export type CalendarProps = {
+ displayName?: string
+ description?: string
+ color?: string
+ components?: string[]
+}
+
+/** Build calendar property XML elements */
+export function buildCalendarPropsXml(props: CalendarProps): string[] {
+ const elements: string[] = []
+
+ if (props.displayName !== undefined && props.displayName !== null && typeof props.displayName === 'string') {
+ elements.push(xmlProp('D', 'displayname', props.displayName))
+ }
+ if (props.description !== undefined && props.description !== null && typeof props.description === 'string') {
+ elements.push(xmlProp('C', 'calendar-description', props.description))
+ }
+ if (props.color !== undefined && props.color !== null && typeof props.color === 'string') {
+ elements.push(xmlProp('A', 'calendar-color', props.color))
+ }
+ if (props.components && props.components.length > 0) {
+ const comps = props.components.map((c) => ``).join('')
+ elements.push(`${comps}`)
+ }
+
+ return elements
+}
+
+/** Build MKCALENDAR request body */
+export function buildMkCalendarXml(props: CalendarProps): string {
+ const propsXml = buildCalendarPropsXml(props)
+ return `
+
+
+
+ ${propsXml.join('\n ')}
+
+
+`
+}
+
+/** Build PROPPATCH request body */
+export function buildProppatchXml(props: CalendarProps): string {
+ const propsXml = buildCalendarPropsXml(props)
+ return `
+
+
+
+ ${propsXml.join('\n ')}
+
+
+`
+}
+
+// ============================================================================
+// Sharing XML Builders
+// ============================================================================
+
+/** Convert SharePrivilege to XML element */
+export function sharePrivilegeToXml(privilege: SharePrivilege): string {
+ const map: Record = {
+ read: '',
+ 'read-write': '',
+ 'read-write-noacl': '',
+ admin: '',
+ }
+ return map[privilege] ?? ''
+}
+
+/** Parse access object to SharePrivilege */
+export function parseSharePrivilege(access: unknown): SharePrivilege {
+ if (!access) return 'read'
+ const accessObj = access as Record
+ if (accessObj['read-write']) return 'read-write'
+ if (accessObj['admin']) return 'admin'
+ if (accessObj['read-write-noacl']) return 'read-write-noacl'
+ return 'read'
+}
+
+export type ShareeXmlParams = {
+ href: string
+ displayName?: string
+ privilege: SharePrivilege
+ summary?: string
+}
+
+/** Build share set XML for a single sharee */
+export function buildShareeSetXml(params: ShareeXmlParams): string {
+ const privilege = sharePrivilegeToXml(params.privilege)
+ const commonName = params.displayName
+ ? `${escapeXml(params.displayName)}`
+ : ''
+ const summary = params.summary ? `${escapeXml(params.summary)}` : ''
+
+ return `
+
+ ${escapeXml(params.href)}
+ ${commonName}
+ ${summary}
+ ${privilege}
+ `
+}
+
+/** Build CS:share request body */
+export function buildShareRequestXml(sharees: ShareeXmlParams[]): string {
+ const shareesXml = sharees.map(buildShareeSetXml).join('')
+ return `
+
+ ${shareesXml}
+`
+}
+
+/** Build CS:share remove request body */
+export function buildUnshareRequestXml(shareeHref: string): string {
+ return `
+
+
+ ${escapeXml(shareeHref)}
+
+`
+}
+
+/** Build invite-reply request body */
+export function buildInviteReplyXml(inReplyTo: string, accept: boolean): string {
+ return `
+
+ ${escapeXml(inReplyTo)}
+
+`
+}
+
+// ============================================================================
+// Sync XML Builders
+// ============================================================================
+
+export type SyncCollectionParams = {
+ syncToken: string
+ syncLevel?: number | 'infinite'
+}
+
+/** Build sync-collection REPORT body */
+export function buildSyncCollectionXml(params: SyncCollectionParams): string {
+ return `
+
+ ${escapeXml(params.syncToken)}
+ ${params.syncLevel ?? 1}
+
+
+
+
+`
+}
+
+// ============================================================================
+// Principal Search XML Builder
+// ============================================================================
+
+/** Build principal-property-search REPORT body */
+export function buildPrincipalSearchXml(query: string): string {
+ return `
+
+
+
+
+
+ ${escapeXml(query)}
+
+
+
+
+
+`
+}
+
+// ============================================================================
+// DAV Request Helpers
+// ============================================================================
+
+export type DavRequestOptions = {
+ url: string
+ method: AllowedMethods
+ body: string
+ headers?: Record
+ fetchOptions?: RequestInit
+ contentType?: string
+}
+
+/** Execute a DAV request with standard error handling */
+export async function executeDavRequest(options: DavRequestOptions): Promise {
+ try {
+ // Use fetch directly for methods that davRequest doesn't handle well
+ // POST is included because CalDAV sharing requires specific Content-Type handling
+ const useDirectFetch = ['PROPPATCH', 'DELETE', 'POST'].includes(options.method);
+
+ if (useDirectFetch) {
+ const response = await fetch(options.url, {
+ method: options.method,
+ headers: {
+ 'Content-Type': options.contentType ?? 'application/xml; charset=utf-8',
+ ...options.headers,
+ },
+ body: options.body || undefined,
+ ...options.fetchOptions,
+ });
+
+ if (!response.ok && response.status !== 204 && response.status !== 207) {
+ const errorText = await response.text().catch(() => '');
+ console.error(`[CalDAV] ${options.method} request failed:`, {
+ url: options.url,
+ status: response.status,
+ error: errorText,
+ });
+ return {
+ success: false,
+ error: `Request failed: ${response.status} ${errorText}`,
+ status: response.status,
+ };
+ }
+
+ return { success: true };
+ }
+
+ // Use davRequest for standard WebDAV methods
+ const responses = await davRequest({
+ url: options.url,
+ init: {
+ method: options.method as DAVMethods,
+ headers: {
+ 'Content-Type': options.contentType ?? 'application/xml; charset=utf-8',
+ ...options.headers,
+ },
+ body: options.body,
+ },
+ fetchOptions: options.fetchOptions,
+ })
+
+ const response = responses[0]
+ if (!response?.ok && response?.status !== 204) {
+ return {
+ success: false,
+ error: `Request failed: ${response?.status}`,
+ status: response?.status,
+ }
+ }
+
+ return { success: true }
+ } catch (error) {
+ return {
+ success: false,
+ error: `Request failed: ${error instanceof Error ? error.message : String(error)}`,
+ }
+ }
+}
+
+/** Standard PROPFIND props for calendar fetching */
+export const CALENDAR_PROPS = {
+ [`${DAVNamespaceShort.CALDAV}:calendar-description`]: {},
+ [`${DAVNamespaceShort.CALDAV}:calendar-timezone`]: {},
+ [`${DAVNamespaceShort.DAV}:displayname`]: {},
+ [`${DAVNamespaceShort.CALDAV_APPLE}:calendar-color`]: {},
+ [`${DAVNamespaceShort.CALENDAR_SERVER}:getctag`]: {},
+ [`${DAVNamespaceShort.DAV}:resourcetype`]: {},
+ [`${DAVNamespaceShort.CALDAV}:supported-calendar-component-set`]: {},
+ [`${DAVNamespaceShort.DAV}:sync-token`]: {},
+} as const
+
+/** Execute PROPFIND with error handling */
+export async function executePropfind(
+ url: string,
+ props: Record,
+ options?: {
+ headers?: Record
+ fetchOptions?: RequestInit
+ depth?: '0' | '1' | 'infinity'
+ }
+): Promise> {
+ try {
+ const response = await propfind({
+ url,
+ props,
+ headers: options?.headers,
+ fetchOptions: options?.fetchOptions,
+ depth: options?.depth ?? '0',
+ })
+
+ const rs = response[0]
+ if (!rs.ok) {
+ return {
+ success: false,
+ error: `PROPFIND failed: ${rs.status}`,
+ status: rs.status,
+ }
+ }
+
+ return { success: true, data: rs.props as T }
+ } catch (error) {
+ return {
+ success: false,
+ error: `PROPFIND failed: ${error instanceof Error ? error.message : String(error)}`,
+ }
+ }
+}
+
+// ============================================================================
+// Response Parsing Helpers
+// ============================================================================
+
+/** Parse supported-calendar-component-set from PROPFIND response */
+export function parseCalendarComponents(supportedCalendarComponentSet: unknown): string[] | undefined {
+ if (!supportedCalendarComponentSet) return undefined
+
+ const comp = (supportedCalendarComponentSet as Record).comp
+ if (Array.isArray(comp)) {
+ return comp
+ .map((sc: Record) => (sc._attributes as Record)?.name)
+ .filter(Boolean)
+ }
+ const name = (comp as Record)?._attributes as Record | undefined
+ return name?.name ? [name.name] : undefined
+}
+
+/** Parse share status from invite response */
+export function parseShareStatus(
+ accepted: unknown,
+ noResponse: unknown
+): 'pending' | 'accepted' | 'declined' {
+ if (accepted) return 'accepted'
+ if (noResponse) return 'pending'
+ return 'declined'
+}
+
+/** Extract calendar URL from event URL */
+export function getCalendarUrlFromEventUrl(eventUrl: string): string {
+ const parts = eventUrl.split('/')
+ parts.pop() // Remove filename
+ return parts.join('/') + '/'
+}
+
+/** Extract calendar ID from calendar URL (e.g., /calendars/user/calendar-id/ -> calendar-id) */
+export function getCalendarIdFromUrl(calendarUrl: string): string {
+ const parts = calendarUrl.replace(/\/$/, '').split('/')
+ return parts[parts.length - 1]
+}
+
+// ============================================================================
+// Error Handling
+// ============================================================================
+
+/** Wrap async operation with standard error handling */
+export async function withErrorHandling(
+ operation: () => Promise,
+ errorPrefix: string
+): Promise> {
+ try {
+ const data = await operation()
+ return { success: true, data }
+ } catch (error) {
+ return {
+ success: false,
+ error: `${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`,
+ }
+ }
+}
diff --git a/src/frontend/apps/calendars/src/features/calendar/services/dav/constants.ts b/src/frontend/apps/calendars/src/features/calendar/services/dav/constants.ts
new file mode 100644
index 0000000..8e5db47
--- /dev/null
+++ b/src/frontend/apps/calendars/src/features/calendar/services/dav/constants.ts
@@ -0,0 +1,16 @@
+export const attendeeRoleTypes = [
+ 'CHAIR',
+ 'REQ-PARTICIPANT',
+ 'OPT-PARTICIPANT',
+ 'NON-PARTICIPANT',
+] as const
+
+export const availableViews = [
+ 'timeGridDay',
+ 'timeGridWeek',
+ 'dayGridMonth',
+ 'listDay',
+ 'listWeek',
+ 'listMonth',
+ 'listYear',
+] as const