natelindev / tsdav

WebDAV, CALDAV, and CARDDAV client for Nodejs and the Browser
https://tsdav.vercel.app
MIT License
227 stars 37 forks source link

can't get VTODO items from Fastmail CalDAV server #207

Closed jdpipe closed 1 month ago

jdpipe commented 1 month ago

Hi there

This script is listing out zero VTODO items from a 'calendar' hosted on fastmail:

import dotenv from 'dotenv';
import { v4 as uuidv4 } from 'uuid';
import { DAVClient } from 'tsdav';
import ICAL from 'ical.js';

// Load environment variables
dotenv.config();
const caldavUrl = process.env.CALDAV_SERVER;
const username = process.env.CALDAV_EMAIL;
const password = process.env.CALDAV_PASS;
const targetCalendarName = process.env.CALDAV_CALNAME;

async function main() {
    try {
        // Create the DAV client
        const client = new DAVClient({
            serverUrl: caldavUrl,
            credentials: {
                username: username,
                password: password,
            },
            authMethod: 'Basic',
            defaultAccountType: 'caldav',
        });

        // Login to the server
        await client.login();

        // Get the list of calendars
        const calendars = await client.fetchCalendars();

        if (!calendars.length) {
            console.log("No calendars found.");
            process.exit(1);
        }

        // Find the target calendar
        let mycal = null;
        for (let calendar of calendars) {
            const displayName = calendar.displayName;
            console.log(`Calendar: ${displayName}`);
            if (displayName === targetCalendarName) {
                mycal = calendar;
                break;
            }
        }

        if (!mycal) {
            console.log(`Target calendar '${targetCalendarName}' not found.`);
            process.exit(1);
        }

        // Check for VTODO support
        const supportedComponents = mycal.components;
        console.log("Supported components:", supportedComponents);

        if (!supportedComponents.includes('VTODO')) {
            console.log("This calendar does not support tasks (VTODO).");
            process.exit(1);
        }

        // Fetch calendar objects with depth: 1
        const calendarObjects = await client.fetchCalendarObjects({
            calendar: mycal,
            depth: 'infinity',
        });

        if (!calendarObjects.length) {
            console.log("No calendar objects found.");
        } else {
            // Filter and display VTODOs
            const vtodos = calendarObjects.filter(obj => {
                const icalComponent = ICAL.Component.fromString(obj.data);
                return icalComponent.getFirstSubcomponent('vtodo') !== null;
            });

            if (vtodos.length === 0) {
                console.log("No VTODOs found.");
            } else {
                console.log("VTODOs:");
                vtodos.forEach(vtodoObj => {
                    const icalComponent = ICAL.Component.fromString(vtodoObj.data);
                    const vtodoComponent = icalComponent.getFirstSubcomponent('vtodo');

                    const summary = vtodoComponent.getFirstPropertyValue('summary');
                    const description = vtodoComponent.getFirstPropertyValue('description');
                    const dueDate = vtodoComponent.getFirstPropertyValue('due');

                    console.log(`Summary: ${summary}`);
                    console.log(`Description: ${description}`);
                    if (dueDate) {
                        console.log(`Due Date: ${dueDate.toString()}`);
                    } else {
                        console.log("Due Date: Not set");
                    }
                    console.log('---');
                });
            }
        }

        // Create a VTODO using ical.js
        const vcalendar = new ICAL.Component(['vcalendar', [], []]);
        vcalendar.addPropertyWithValue('prodid', '-//My Product//EN');
        vcalendar.addPropertyWithValue('version', '2.0');

        const vtodo = new ICAL.Component('vtodo');
        const now = ICAL.Time.now();
        const dueTime = now.clone();
        dueTime.adjust(0, 0, 1, 0); // Set due time to one day from now

        vtodo.addPropertyWithValue('uid', uuidv4());
        vtodo.addPropertyWithValue('summary', 'another VTODO');
        vtodo.addPropertyWithValue('description', 'This is another VTODO.');
        vtodo.addPropertyWithValue('due', dueTime.toString());
        vtodo.addPropertyWithValue('status', 'NEEDS-ACTION');
        vtodo.addPropertyWithValue('created', now.toString());

        vcalendar.addSubcomponent(vtodo);

        const vtodoStr = vcalendar.toString();

        // Create and save the VTODO
        await client.createCalendarObject({
            calendar: mycal,
            iCalString: vtodoStr,
        });

        console.log(`VTODO created successfully in calendar '${mycal.displayName}!'`);
    } catch (err) {
        console.log(`An error occurred: ${err.message}`);
    }
}

main();

However, this script in Python works correctly, and lists the items. I think there is an error in tsdav, or else some special handling/approach is needed for Fastmail?

from datetime import datetime, timedelta
from dotenv import load_dotenv
import os, sys
import caldav
import vobject

# Load environment variables
load_dotenv()
caldav_url = os.environ['CALDAV_SERVER']
username = os.environ['CALDAV_EMAIL']
password = os.environ['CALDAV_PASS']
target_calendar_name = os.environ['CALDAV_CALNAME']

# Connect to the CalDAV server
client = caldav.DAVClient   (url=caldav_url, username=username, password=password)

# Get the principal (account)
principal = client.principal()

# Get the list of calendars
calendars = principal.calendars()

if not calendars:
    print("No calendars found.")
    exit(1)

# Find the target calendar
mycal = None
for calendar in calendars:
    displayname = calendar.get_display_name()
    print(f"Calendar: {displayname}")
    if displayname == target_calendar_name:
        mycal = calendar

if not mycal:
    print(f"Target calendar '{target_calendar_name}' not found.")
    sys.exit(1)

# check for VTODO support.

print(f"Calendar: {calendar.name}")

# Fetch properties
properties = mycal.get_properties([caldav.elements.dav.DisplayName(), caldav.elements.cdav.SupportedCalendarComponentSet()])

# Check for VTODO support
supported = mycal.get_supported_components()
print("supported:",supported)

if 'VTODO' not in supported:
    print("  This calendar does not support tasks (VTODO).")
    exit(1)

# List all VTODOs in the selected calendar
vtodos = mycal.todos()
if not vtodos:
    print("No VTODOs found.")
else:
    print("VTODOs:")
    for vtodo in vtodos:
        todo_data = vobject.readOne(vtodo.data)
        summary = todo_data.vtodo.summary.value if 'summary' in todo_data.vtodo.contents else 'No summary'
        description = todo_data.vtodo.description.value if 'description' in todo_data.vtodo.contents else 'No description'
        due = todo_data.vtodo.due.value.strftime('%Y-%m-%d %H:%M:%S') if 'due' in todo_data.vtodo.contents else 'No due date'

        print(f"Summary: {summary}")
        print(f"Description: {description}")
        print(f"Due Date: {due}")
        print('---')

# Create a VTODO using vobject
vtodo = vobject.iCalendar()
vtodo.add('VTODO')
vtodo.vtodo.add('UID').value = datetime.now().strftime('%Y%m%dT%H%M%S')
vtodo.vtodo.add('DTSTAMP').value = datetime.utcnow()
vtodo.vtodo.add('SUMMARY').value = 'Sample VTODO'
vtodo.vtodo.add('DESCRIPTION').value = 'This is a another VTODO.'
vtodo.vtodo.add('DUE').value = datetime.utcnow() + timedelta(days=1)
vtodo.vtodo.add('STATUS').value = 'NEEDS-ACTION'

# Convert VTODO to a string
vtodo_str = vtodo.serialize()

# Create and save the VTODO using the correct method
try:
    mycal.add_todo(vtodo_str)
    print(f"VTODO created successfully in calendar '{mycal.get_display_name()}!'")
except caldav.error.AuthorizationError as e:
    print(f"AuthorizationError for calendar '{mycal.get_display_name()}': {e}")
except Exception as e:
    print(f"An error occurred: {e}")
natelindev commented 1 month ago

will take a look next week.

natelindev commented 1 month ago

duplicate to #192