iCloud: find calendars whatever way Apple writes their component list
build-container / build (push) Successful in 2m6s

Apple's listing was recognised as calendars but then discarded by the events/tasks filter.
Read component names from anywhere in the entry (any quoting or prefix), treat a missing or
empty list as events, and keep calendars as event calendars if every one would otherwise be
dropped. Errors now show each item with its component types.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-20 15:57:06 -04:00
1 parent a4e0e24ab7
commit d3642a7736
1 file changed
+29 -18
+29 -18
View File
@@ -51,6 +51,31 @@ const REPORT = (from, to) => `<?xml version="1.0"?><c:calendar-query xmlns:d="DA
const calId = href => 'icloud:' + crypto.createHash('sha1').update(href).digest('hex').slice(0, 10);
/** Which of the collections in a PROPFIND listing are calendars, and do they hold events and/or tasks? Tolerates every XML style seen in the wild. */
function parseCollections(xml, baseUrl) {
const found = [], seen = [], candidates = [];
for (const r of responses(xml)) {
const rt = tag(r, 'resourcetype') || '';
// <calendar xmlns="urn:ietf:params:xml:ns:caldav"/>, <C:calendar/>, <cal:calendar></cal:calendar> ... the lookahead keeps
// calendar-proxy-read, calendar-home-set etc. from matching.
const isCal = /<(?:[\w-]+:)?calendar(?=[\s/>])/i.test(rt);
const comps = [...r.matchAll(/<(?:[\w-]+:)?comp\b[^>]*?\bname\s*=\s*["']([A-Za-z]+)["']/gi)].map(m => m[1].toUpperCase());
seen.push(isCal ? `calendar[${comps.join('+') || '?'}]` : (rt.replace(/<(?:[\w-]+:)?([\w-]+)[^>]*>/g, '$1 ').trim() || 'other').slice(0, 30));
if (!isCal) continue;
const events = !comps.length || comps.includes('VEVENT'), todos = comps.includes('VTODO');
const href = abs(hrefOf(r), baseUrl);
const color = (tag(r, 'calendar-color') || '').trim().slice(0, 7);
const entry = { id: calId(href), href, name: decode(tag(r, 'displayname') || 'Calendar').trim() || 'Calendar', color: /^#[0-9a-f]{6}$/i.test(color) ? color : '#d9453a', enabled: true, events, todos };
candidates.push({ ...entry, events: true, todos: false });
if (events || todos) found.push(entry);
}
// Safety net: if every collection that is a calendar was filtered out, keep them all as event calendars rather than none.
const out = found.length ? found : candidates;
out.seen = seen; out.found = out;
return out;
}
/** Sign in, find the calendars, and remember everything (encrypted). Throws a readable message on failure. */
async function connect(appleId, password) {
const o = { user: String(appleId || '').trim(), pass: String(password || '').replace(/\s/g, '') };
@@ -66,23 +91,9 @@ async function connect(appleId, password) {
const home = abs(homeHref, b.url);
const c = await dav('PROPFIND', home, { ...o, depth: 1, body: PROPFIND_CALS });
if (c.status >= 400) throw new Error(`iCloud answered ${c.status} when listing the calendars`);
const calendars = [], seen = [];
for (const r of responses(c.text)) {
const rt = tag(r, 'resourcetype') || '';
// Apple writes <calendar xmlns="urn:ietf:params:xml:ns:caldav"/> (with attributes, and often no prefix); the lookahead keeps
// calendar-proxy-read, calendar-home-set etc. from matching.
const isCal = /<(?:[\w-]+:)?calendar(?=[\s/>])/i.test(rt);
seen.push(isCal ? 'calendar' : (rt.replace(/<(?:[\w-]+:)?([\w-]+)[^>]*>/g, '$1 ').trim() || 'other').slice(0, 30));
if (!isCal) continue;
const comps = tag(r, 'supported-calendar-component-set');
const events = !comps || /name="VEVENT"/i.test(comps), todos = !!comps && /name="VTODO"/i.test(comps);
if (!events && !todos) continue;
const href = abs(hrefOf(r), c.url);
const color = (tag(r, 'calendar-color') || '').trim().slice(0, 7);
calendars.push({ id: calId(href), href, name: decode(tag(r, 'displayname') || 'Calendar').trim() || 'Calendar', color: /^#[0-9a-f]{6}$/i.test(color) ? color : '#d9453a', enabled: true, events, todos });
}
if (!calendars.length) throw new Error(`Signed in, but no calendars were found (iCloud listed ${seen.length} item${seen.length === 1 ? '' : 's'}: ${seen.slice(0, 6).join(', ') || 'none'}). Make sure Calendar is turned on for this Apple ID in iCloud settings.`);
save({ user: o.user, pass: o.pass, calendars });
const calendars = parseCollections(c.text, c.url);
if (!calendars.found.length) throw new Error(`Signed in, but no calendars were found (iCloud listed ${calendars.seen.length} item${calendars.seen.length === 1 ? '' : 's'}: ${calendars.seen.slice(0, 8).join(', ') || 'none'}). Make sure Calendar is turned on for this Apple ID in iCloud settings.`);
save({ user: o.user, pass: o.pass, calendars: calendars.found });
return status();
}
@@ -144,4 +155,4 @@ async function deleteEvent(id, href, etag) {
if (r.status >= 400 && r.status !== 404) throw new Error('iCloud refused the delete (' + r.status + ')');
}
module.exports = { connect, status, toggle, enabled, todoLists, listTodoRaw, listRaw, getResource, putEvent, deleteEvent, wipe, isCal: id => /^icloud:[0-9a-f]{10}$/.test(id || '') };
module.exports = { parseCollections, connect, status, toggle, enabled, todoLists, listTodoRaw, listRaw, getResource, putEvent, deleteEvent, wipe, isCal: id => /^icloud:[0-9a-f]{10}$/.test(id || '') };