Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICloud calendar support #305

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

ICloud calendar support #305

wants to merge 2 commits into from

Conversation

EasyHard
Copy link

icloud calendar CalDav has three places that are implemented differently.

  1. It can return empty string "" on status:Dav for a calendar.
  2. for each .ics, it returns two props, one without resource type.
  3. It doesn't use Dav namespace for props

icloud calendar CalDav has three places that are implemented differently.
1. It can return empty string "" on status:Dav for a calendar.
2. for each .ics, it returns two props, one without resource type.
3. It doesn't use Dav namespace for props
@EasyHard EasyHard mentioned this pull request Aug 25, 2024
@jackkamm
Copy link
Collaborator

Thanks for investigating this.

I'd like to understand a bit better what is going on before merging this. Could you post a few examples of the HTTP responses you're getting from iCloud, that this patch fixes? In case it helps, I've added an extra level of debug verbosity, so that URL requests and responses are printed to *org-caldav-debug* when syncing. To use this, pull/merge the latest master branch, and set org-caldav-debug-level to 3.

Additional comments:

I think it sounds reasonable to treat empty string as 404 status, but should add a comment that this behavior is a workaround for iCalendar.

It seems strange to be adding in resourcetype tag into the org-caldav-url-dav-get-properties result, even when property isn't resourcetype (e.g when it's getetag instead).

Injecting "DAV:" as default namespace may be reasonable, but I'm not sure the propstat tag is the best place. Also it seems rather surprising that in the XML response from iCloud, the duplicate 404 entry you delete has xmlns="DAV:", but the 200 entry you keep is missing it.

@MiiCode2
Copy link

I have org-caldav working with Apple's icloud.

This is not a patch, but I will show you what the problem(s) are and how to make this work.

Note: I am using the latest Org source code and the latest org-caldav.

So the main two issues as pointed out by EasyHard are url-dav.el not being able to handle empty property tags and the empty string for org-caldav-check-connection.

===============

Here is what we get from iCloud:

<propstat>
  <prop></prop>
  <status>HTTP/1.1 200 OK</status>
</propstat>

However url-dav.el is expecting something along the lines of this:

<prop>
  <resourcetype><collection/><calendar xmlns="urn:ietf:params:xml:ns:caldav"/></resourcetype>
</prop>

Here is a parialsample from one of my requests:

...
    <response xmlns="DAV:">
        <href>/99660340/calendars/BFEA2FAF-178E-4EA7-92F8-763C6D953707/</href>

    <propstat>
                <prop>


            <resourcetype xmlns="DAV:"><collection/><calendar xmlns="urn:ietf:params:xml:ns:caldav"/></resourcetype>


                </prop>
                <status>HTTP/1.1 200 OK</status>
    </propstat>


</response>

    <response xmlns="DAV:">
        <href>/99660340/calendars/BFEA2FAF-178E-4EA7-92F8-763C6D953707/62c2dd25-d3b0-464b-a6b0-98be19354ed8.ics</href>
    <propstat>
                <prop>




                </prop>
                <status>HTTP/1.1 200 OK</status>
    </propstat>

                <propstat>
        <prop>


                                <resourcetype xmlns="DAV:"/>


            </prop>
        <status>HTTP/1.1 404 Not Found</status>
                </propstat>

</response>
...

I asked Grok the following:

Is This Apple’s Fault?

  • Standards Compliance: iCloud’s response is allowed by the standards:

  • RFC 4918 permits to be empty if no properties are returned for a resource.

  • RFC 4791 doesn’t mandate for event resources— applies to collections, not individual .ics files.

  • Returning with 200 OK for events (indicating "no here") is technically valid, if unconventional.

Apple’s Choice: iCloud could include a (e.g., ) to indicate it’s an event, but it opts for minimalism—an empty —which is within spec but not the most informative approach.

Fault?: Not really—it’s not a violation, but it’s an edge case that other servers (e.g., DAViCal, Zoho) handle differently by avoiding empty tags or returning 404/403 instead of 200 OK for unapplicable properties.

The my/url-dav-process-DAV:prop code shown below addresses the issue with url-dav-process-DAV:prop found in url-dav.el

===============

Regarding the empty string issue when performing the org-caldav-check-connection found in org-caldav.el

The code (my/org-caldav-check-connection) shown below fixes edge case: Handles iCloud’s potential empty status string, converting it to 404 to match the original’s empty-calendar logic.

===============

THE SOLUTION

This is what you need to do to get Apple's iCloud to work. Until proper patches are provided to the source code, this is the simplest way to patch the existing code to get this functionality working.

In your init.el or similar place add the following code:

     ;; Override url-dav-process-DAV:prop to handle empty <prop> tags
      (defun my/url-dav-process-DAV:prop (node)
        "Process a DAV:prop node, tolerating empty props without error."
        (let ((children (xml-node-children node))
              (node-type nil)
              (props nil)
              (value nil))
          (if (not children)
              (progn
                (message "DEBUG: Skipping empty DAV:prop node")
                props)  ;; Return empty props list if no children
            (while children
              (setq node (car children)
                    node-type (intern
                               (or
                                (cdr-safe (assq url-dav-datatype-attribute
                                                (xml-node-attributes node)))
                                "unknown"))
                    value nil)
              (pcase node-type
                ((or 'dateTime.iso8601tz 'dateTime.iso8601 'dateTime.tz 'dateTime.rfc1123 'dateTime 'date)
                 (setq value (url-dav-process-date-property node)))
                ('int
                 (setq value (url-dav-process-integer-property node)))
                ((or 'number 'float)
                 (setq value (url-dav-process-number-property node)))
                ('boolean
                 (setq value (url-dav-process-boolean-property node)))
                ('uri
                 (setq value (url-dav-process-uri-property node)))
                (_
                 (if (not (eq node-type 'unknown))
                     (url-debug 'dav "Unknown data type in url-dav-process-prop: %s" node-type))
                 (setq value (url-dav-dispatch-node node))))
              (setq props (plist-put props (xml-node-name node) value)
                    children (cdr children)))
            props)))
      (advice-add 'url-dav-process-DAV:prop :override #'my/url-dav-process-DAV:prop)

Again in your init.el or similar place add the following code:

       ;; Override org-caldav-check-connection to handle empty status
       (defun my/org-caldav-check-connection ()
         "Check connection by doing a PROPFIND on CalDAV URL, handling empty status."
         (org-caldav-debug-print 1 (format "Check connection for %s."
                                           (org-caldav-events-url)))
         (org-caldav-check-dav (org-caldav-events-url))
         (let* ((output (org-caldav-url-dav-get-properties
                         (org-caldav-events-url) "resourcetype"))
                (status (plist-get (cdar output) 'DAV:status)))
           (when (and (stringp status) (string= status ""))
             (message "DEBUG: Converting empty status to 404")
             (setq status 404))
           (unless (or (= (/ status 100) 2)
                       (= status 404))
             (org-caldav-debug-print 1 "Got error status from PROPFIND: " output)
             (error "Could not query CalDAV URL %s." (org-caldav-events-url)))
           (if (= status 404)
               (progn
                 (org-caldav-debug-print 1 "Got 404 status - assuming calendar is new and empty.")
                 (setq org-caldav-empty-calendar t))
             (when (= (length output) 1)
               (org-caldav-debug-print 1 "This is an empty calendar. Setting flag.")
               (setq org-caldav-empty-calendar t)))
           t))
       (advice-add 'org-caldav-check-connection :override #'my/org-caldav-check-connection)

Optionally, (see attached code) if you want to use alarms in your org entries such as:

* TODO Pay Auto Insurance
  DEADLINE: <2025-03-27 Thu 10:00>
  :PROPERTIES:
  :EXPORT_ICALENDAR_ALARM: -PT7D -PT1D
  :DESCRIPTION: Amount Due $ 200.00
  :END:

With the above entry, you will end up with a event in your Apple Calendar called in my case TODO-Pay Auto Insurance and it will have an alarm reminder of 7 days before and 1 day before. The Notes field will contain Amount Due $200.00.

If you change the title on your Apple Calendar to DONE-Pay Auto Insurance, during the next sync, the org document will reflect that and a further sync will remove the DONE item from you Apple Calendar. Please note, alarms are currently not synced back to the org document, in other words they currently only go one way.
01-org-caldav.txt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants