emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
* [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
@ 2023-03-26 18:56 Jack Kamm
  2023-03-27 11:59 ` Ihor Radchenko
  0 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-03-26 18:56 UTC (permalink / raw)
  To: emacs-orgmode; +Cc: mail

[-- Attachment #1: Type: text/plain, Size: 1636 bytes --]

Hello,

The attached 2 patches add support for exporting unscheduled tasks and
repeating tasks to iCalendar, respectively.

For patch 1 (unscheduled tasks):

Currently, ox-icalendar does not allow creating an iCalendar task
without a scheduled start date. If an Org TODO is missing a SCHEDULED
timestamp, then ox-icalendar sets today as the scheduled start date for
the exported task.

Patch 1 changes this by adding a new customization
org-icalendar-todo-force-scheduling. When non-nil, the start date is set
to today (same as the current behavior). When nil, unscheduled Org TODOs
are instead exported without a start date.

I also propose the default value to be nil. Note, this is
backwards-incompatible with the previous behavior!

But I think it should be the default anyways, because IMO it is the more
correct and useful behavior. An iCalendar VTODO without a DTSTART
property is valid, and has the same meaning as an Org TODO without a
SCHEDULED timestamp. Also, all the iCalendar programs I have tried
support unscheduled tasks, including Thunderbird, Evolution, Nextcloud,
and Tasks.org.

For patch 2 (repeating timestamps):

I add recurrence rule (RRULE) export for repeating SCHEDULED and
DEADLINE timestamps in TODOs, similar to how repeating non-TODO events
are currently handled.

The main complication here is that iCalendar's RRULE applies to both
DTSTART and DUE properties; by contrast, Org's SCHEDULED and DEADLINE
timestamps may have different repeaters. I am not sure the best way to
handle the case where SCHEDULED and DEADLINE have different repeaters,
so in that case I issue a warning and skip the repeater.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-ox-icalendar-Allow-exporting-unscheduled-VTODOs.patch --]
[-- Type: text/x-patch, Size: 4258 bytes --]

From 1bd268ab260d5077d7456c0d64fea36128772f86 Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sun, 26 Mar 2023 07:43:53 -0700
Subject: [PATCH 1/2] ox-icalendar: Allow exporting unscheduled VTODOs

* lisp/ox-icalendar.el (org-icalendar-todo-force-scheduling): New
option to revert to previous export behavior of unscheduled TODOs.
(org-icalendar--vtodo): Don't force unscheduled TODOs to have a
scheduled start time of today, unless
`org-icalendar-todo-force-scheduling' is set.
---
 etc/ORG-NEWS         | 15 +++++++++++++++
 lisp/ox-icalendar.el | 32 +++++++++++++++++++++-----------
 2 files changed, 36 insertions(+), 11 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index ac233a986..fb4f82b29 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -23,6 +23,15 @@ If you still want to use python-mode with ob-python, you might
 consider [[https://gitlab.com/jackkamm/ob-python-mode-mode][ob-python-mode-mode]], where the code to support python-mode
 has been ported to.
 
+*** Icalendar export of TODOs no longer forces a start time
+
+For TODOs without a scheduled start time, ox-icalendar no longer
+forces them to have a scheduled start time of today when exporting.
+This makes it possible to create icalendar TODOs without a start time.
+
+To revert to the old behavior, set the new custom option
+~org-icalendar-todo-force-scheduling~ to non-nil.
+
 ** New and changed options
 *** New ~org-cite-natbib-export-bibliography~ option defining fallback bibliography style
 
@@ -111,6 +120,12 @@ backend used for evaluation of ClojureScript.
 official [[https://clojure.org/guides/deps_and_cli][Clojure CLI tools]].
 The command can be customized with ~ob-clojure-cli-command~.
 
+*** New ~org-icalendar-todo-force-scheduling~ option for old ox-icalendar TODO scheduling behavior
+
+Set ~org-icalendar-todo-force-scheduling~ to non-nil to revert to the
+old ox-icalendar TODO export behavior, that forced all exported TODOs
+to have a scheduled start time.
+
 ** New features
 *** Add support for ~logind~ idle time in ~org-user-idle-seconds~
 
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 81a77a770..63aefcc84 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -231,6 +231,12 @@ (defcustom org-icalendar-include-todo nil
 	  (repeat :tag "Specific TODO keywords"
 		  (string :tag "Keyword"))))
 
+(defcustom org-icalendar-todo-force-scheduling nil
+  "Non-nil means unscheduled tasks are exported as scheduled.
+The current date is used as the scheduled time for such tasks."
+  :group 'org-export-icalendar
+  :type 'boolean)
+
 (defcustom org-icalendar-include-bbdb-anniversaries nil
   "Non-nil means a combined iCalendar file should include anniversaries.
 The anniversaries are defined in the BBDB database."
@@ -776,21 +782,25 @@ (defun org-icalendar--vtodo
 Return VTODO component as a string."
   (let ((start (or (and (memq 'todo-start org-icalendar-use-scheduled)
 			(org-element-property :scheduled entry))
-		   ;; If we can't use a scheduled time for some
-		   ;; reason, start task now.
-		   (let ((now (decode-time)))
-		     (list 'timestamp
-			   (list :type 'active
-				 :minute-start (nth 1 now)
-				 :hour-start (nth 2 now)
-				 :day-start (nth 3 now)
-				 :month-start (nth 4 now)
-				 :year-start (nth 5 now)))))))
+                   (when org-icalendar-todo-force-scheduling
+		     ;; If we can't use a scheduled time for some
+		     ;; reason, start task now.
+                     (let ((now (decode-time)))
+		       (list 'timestamp
+			     (list :type 'active
+				   :minute-start (nth 1 now)
+				   :hour-start (nth 2 now)
+				   :day-start (nth 3 now)
+				   :month-start (nth 4 now)
+				   :year-start (nth 5 now))))))))
     (org-icalendar-fold-string
      (concat "BEGIN:VTODO\n"
 	     "UID:TODO-" uid "\n"
 	     (org-icalendar-dtstamp) "\n"
-	     (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n"
+             (when start
+               (concat (org-icalendar-convert-timestamp
+                        start "DTSTART" nil timezone)
+                       "\n"))
 	     (and (memq 'todo-due org-icalendar-use-deadline)
 		  (org-element-property :deadline entry)
 		  (concat (org-icalendar-convert-timestamp
-- 
2.39.2


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-ox-icalendar-Support-repeating-timestamps-in-TODOs.patch --]
[-- Type: text/x-patch, Size: 5652 bytes --]

From 8348f5b8c56087f0fb8cdd775a816f63cb57f38f Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sun, 26 Mar 2023 10:37:47 -0700
Subject: [PATCH 2/2] ox-icalendar: Support repeating timestamps in TODOs

* lisp/ox-icalendar.el (org-icalendar--rrule): New helper function to
generate RRULE.
(org-icalendar--vevent): Use `org-icalendar--rrule' instead of
generating the RRULE directly.
(org-icalendar--vtodo): Generate RRULE for repeating scheduled and
deadline timestamps.
---
 etc/ORG-NEWS         | 13 ++++++++++++
 lisp/ox-icalendar.el | 50 +++++++++++++++++++++++++++++++++-----------
 2 files changed, 51 insertions(+), 12 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index fb4f82b29..3919b240e 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -159,6 +159,19 @@ Running shell blocks with the ~:session~ header freezes Emacs until
 execution completes.  The new ~:async~ header allows users to continue
 editing with Emacs while a ~:session~ block executes.
 
+*** Add support for repeating tasks in iCalendar export
+
+Repeating Scheduled and Deadline timestamps in TODOs are now exported
+as recurring tasks in iCalendar export.
+
+Note that in Org-mode, the repeaters for the Scheduled and Deadline
+timestamps can be different; whereas in iCalendar, the recurrence rule
+applies to both the scheduled start time and the deadline due date.
+
+In case the timestamp repeaters contradict, the correct export
+behavior is not well-defined.  Currently, Org-mode will issue a
+warning and skip the repeaters in this case.
+
 ** Miscellaneous
 *** Remove undocumented ~:target~ header parameter in ~ob-clojure~
 
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 63aefcc84..179795ac9 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -726,6 +726,13 @@ (defun org-icalendar-entry (entry contents info)
        ;; Don't forget components from inner entries.
        contents))))
 
+(defun org-icalendar--rrule (unit value)
+  (format "RRULE:FREQ=%s;INTERVAL=%d\n"
+	  (cl-case unit
+	    (hour "HOURLY") (day "DAILY") (week "WEEKLY")
+	    (month "MONTHLY") (year "YEARLY"))
+	  value))
+
 (defun org-icalendar--vevent
     (entry timestamp uid summary location description categories timezone class)
   "Create a VEVENT component.
@@ -752,12 +759,9 @@ (\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are predefined, others
 	     (org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n"
 	     (org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
 	     ;; RRULE.
-	     (when (org-element-property :repeater-type timestamp)
-	       (format "RRULE:FREQ=%s;INTERVAL=%d\n"
-		       (cl-case (org-element-property :repeater-unit timestamp)
-			 (hour "HOURLY") (day "DAILY") (week "WEEKLY")
-			 (month "MONTHLY") (year "YEARLY"))
-		       (org-element-property :repeater-value timestamp)))
+             (org-icalendar--rrule
+              (org-element-property :repeater-unit timestamp)
+              (org-element-property :repeater-value timestamp))
 	     "SUMMARY:" summary "\n"
 	     (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
 	     (and (org-string-nw-p class) (format "CLASS:%s\n" class))
@@ -792,7 +796,9 @@ (defun org-icalendar--vtodo
 				   :hour-start (nth 2 now)
 				   :day-start (nth 3 now)
 				   :month-start (nth 4 now)
-				   :year-start (nth 5 now))))))))
+				   :year-start (nth 5 now)))))))
+        (due (and (memq 'todo-due org-icalendar-use-deadline)
+                  (org-element-property :deadline entry))))
     (org-icalendar-fold-string
      (concat "BEGIN:VTODO\n"
 	     "UID:TODO-" uid "\n"
@@ -801,11 +807,31 @@ (defun org-icalendar--vtodo
                (concat (org-icalendar-convert-timestamp
                         start "DTSTART" nil timezone)
                        "\n"))
-	     (and (memq 'todo-due org-icalendar-use-deadline)
-		  (org-element-property :deadline entry)
-		  (concat (org-icalendar-convert-timestamp
-			   (org-element-property :deadline entry) "DUE" nil timezone)
-			  "\n"))
+	     (when due
+	       (concat (org-icalendar-convert-timestamp
+			due "DUE" nil timezone)
+		       "\n"))
+             ;; RRULE
+             (let ((start-repeater-unit (org-element-property
+                                         :repeater-unit start))
+                   (start-repeater-value (org-element-property
+                                          :repeater-value start))
+                   (due-repeater-unit (org-element-property
+                                       :repeater-unit due))
+                   (due-repeater-value (org-element-property
+                                        :repeater-value due)))
+               (when (or start-repeater-value due-repeater-value)
+                 (if (and start due
+                          (not (and (eql start-repeater-unit
+                                         due-repeater-unit)
+                                    (eql start-repeater-value
+                                         due-repeater-value))))
+                     (progn (warn "Scheduled and Deadline repeaters are not equal. Skipping repeater export.")
+                            nil)
+                   (org-icalendar--rrule (or start-repeater-unit
+                                             due-repeater-unit)
+                                         (or start-repeater-value
+                                             due-repeater-value)))))
 	     "SUMMARY:" summary "\n"
 	     (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
 	     (and (org-string-nw-p class) (format "CLASS:%s\n" class))
-- 
2.39.2


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-26 18:56 [RFC] ox-icalendar: Unscheduled tasks & repeating tasks Jack Kamm
@ 2023-03-27 11:59 ` Ihor Radchenko
  2023-03-31  5:55   ` Jack Kamm
                     ` (2 more replies)
  0 siblings, 3 replies; 21+ messages in thread
From: Ihor Radchenko @ 2023-03-27 11:59 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> For patch 1 (unscheduled tasks):
>
> Currently, ox-icalendar does not allow creating an iCalendar task
> without a scheduled start date. If an Org TODO is missing a SCHEDULED
> timestamp, then ox-icalendar sets today as the scheduled start date for
> the exported task.
>
> Patch 1 changes this by adding a new customization
> org-icalendar-todo-force-scheduling. When non-nil, the start date is set
> to today (same as the current behavior). When nil, unscheduled Org TODOs
> are instead exported without a start date.
>
> I also propose the default value to be nil. Note, this is
> backwards-incompatible with the previous behavior!
>
> But I think it should be the default anyways, because IMO it is the more
> correct and useful behavior. An iCalendar VTODO without a DTSTART
> property is valid, and has the same meaning as an Org TODO without a
> SCHEDULED timestamp. Also, all the iCalendar programs I have tried
> support unscheduled tasks, including Thunderbird, Evolution, Nextcloud,
> and Tasks.org.

I agree that omitting DTSTART will make more sense.

> For patch 2 (repeating timestamps):
>
> I add recurrence rule (RRULE) export for repeating SCHEDULED and
> DEADLINE timestamps in TODOs, similar to how repeating non-TODO events
> are currently handled.
>
> The main complication here is that iCalendar's RRULE applies to both
> DTSTART and DUE properties; by contrast, Org's SCHEDULED and DEADLINE
> timestamps may have different repeaters. I am not sure the best way to
> handle the case where SCHEDULED and DEADLINE have different repeaters,
> so in that case I issue a warning and skip the repeater.

In the case of different repeaters, we can use RDATE
(https://icalendar.org/iCalendar-RFC-5545/3-8-5-2-recurrence-date-times.html)
and generate occurrences manually sufficiently far into future. ("how
far" should be a defcustom).

However, different repeaters for deadline and schedule are most likely a
mistake - we can report it via org-lint and in ox-icalendar, as warning.

Another scenario we may need to consider is when schedule has a repeater
while deadline does not, and vice versa. The former scenario is probably
valid - a VTODO with limited number of occurrences. The latter is likely
a mistake we should raise warning about. It is also not clear how to
represent moving event deadline in iCalendar.

See more inline comments below.

> +(defcustom org-icalendar-todo-force-scheduling nil
> +  "Non-nil means unscheduled tasks are exported as scheduled.
> +The current date is used as the scheduled time for such tasks."
> +  :group 'org-export-icalendar
> +  :type 'boolean)

Please add :package-version and possibly :safe keywords.
We may also refer to `org-icalendar-include-todo' in the docstring.

> -	     (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n"
> +             (when start
> +               (concat (org-icalendar-convert-timestamp
> +                        start "DTSTART" nil timezone)
> +                       "\n"))

Side note: here, and in other places, we use "\n" as end of line. Yet,
for example
https://icalendar.org/iCalendar-RFC-5545/3-8-2-4-date-time-start.html
prescribes CRLF (\r\n). Also, see
https://orgmode.org/list/87ilgljv6i.fsf@localhost
If you are familiar with iCalendar spec, may you look through the
ox-icalendar code and check other places where we do not conform to the
newline spec?

Ideally, we want a set of private functions ensuring proper prescribed
format for all the used iCalendar syntax entries. Otherwise, we will
keep forgetting about these subtleties.

> +(defun org-icalendar--rrule (unit value)
> +  (format "RRULE:FREQ=%s;INTERVAL=%d\n"

\r\n

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-27 11:59 ` Ihor Radchenko
@ 2023-03-31  5:55   ` Jack Kamm
  2023-03-31 13:07     ` Ihor Radchenko
  2023-04-14 16:57   ` Jack Kamm
  2023-06-11 15:35   ` [PATCH] " Jack Kamm
  2 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-03-31  5:55 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

[-- Attachment #1: Type: text/plain, Size: 1348 bytes --]

Ihor Radchenko <yantar92@posteo.net> writes:

> Side note: here, and in other places, we use "\n" as end of line. Yet,
> for example
> https://icalendar.org/iCalendar-RFC-5545/3-8-2-4-date-time-start.html
> prescribes CRLF (\r\n). Also, see
> https://orgmode.org/list/87ilgljv6i.fsf@localhost
> If you are familiar with iCalendar spec, may you look through the
> ox-icalendar code and check other places where we do not conform to the
> newline spec?

org-icalendar--vtodo is wrapped in org-icalendar-fold-string, so this
"\n" gets converted to CRLF later on.

However you are right that other parts of the iCalendar export have
inconsistent line endings. Currently, VEVENT and VTODO components have
the correct CRLF endings, but the other parts of the VCALENDAR do not
(such as the preamble).

I like your suggestion in the above thread to just wrap the whole
export in `org-icalendar-fold-string'.  Though I think it's slightly
nicer to do it in `org-icalendar--vcalendar' instead of
`org-icalendar-template'.

So, I've attached a standalone patch to do this. It also fixes an issue
with `org-icalendar-fold-string' where the last newline was missing
"\r", and adds a unit test.

Note that fixing the line endings causes a surprising compatibility
issue with org-caldav. I fixed this problem on the org-caldav side, and
made a note in ORG-NEWS.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-ox-icalendar-Use-consistent-CRLF-line-endings.patch --]
[-- Type: text/x-patch, Size: 12143 bytes --]

From 712a4ef09b63b2f6bdec2a3967712be912dce0d2 Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Thu, 30 Mar 2023 22:19:09 -0700
Subject: [PATCH] ox-icalendar: Use consistent CRLF line endings

Fixes issue where the ox-icalendar export uses an inconsistent mix of
dos and unix style line endings.

* lisp/ox-icalendar.el (org-icalendar-fold-string): Don't use "\r"
during the string construction, instead replace "\n" with "\r\n" after
string has been created.  This fixes an issue where the final "\n"
added by `org-element-normalize-string' was missing "\r".
(org-icalendar--vevent): Remove call to `org-icalendar-fold-string'.
(org-icalendar--vtodo): Remove call to `org-icalendar-fold-string'.
(org-icalendar--vcalendar): Wrap in `org-icalendar-fold-string'.
* testing/lisp/test-ox-icalendar.el: New file for unit tests of
ox-icalendar.  Add an initial test for CRLF line endings.

See also:

https://list.orgmode.org/87o7oetneo.fsf@localhost/T/#m3e3eb80f9fc51ba75854b33ebfe9ecdefa2ded24

https://list.orgmode.org/orgmode/87ilgljv6i.fsf@localhost/
---
 etc/ORG-NEWS                      |  12 +++
 lisp/ox-icalendar.el              | 159 +++++++++++++++---------------
 testing/lisp/test-ox-icalendar.el |  46 +++++++++
 3 files changed, 138 insertions(+), 79 deletions(-)
 create mode 100644 testing/lisp/test-ox-icalendar.el

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index ac233a986..9f7d01707 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -23,6 +23,18 @@ If you still want to use python-mode with ob-python, you might
 consider [[https://gitlab.com/jackkamm/ob-python-mode-mode][ob-python-mode-mode]], where the code to support python-mode
 has been ported to.
 
+*** =ox-icalendar.el= line ending fix may affect downstream packages
+
+iCalendar export now uses dos-style CRLF ("\r\n") line endings
+throughout, as required by the iCalendar specification (RFC 5545).
+Previously, the export used an inconsistent mix of dos and unix line
+endings.
+
+This might cause errors in external packages that parse output from
+ox-icalendar.  In particular, older versions of org-caldav may
+encounter issues, and users are advised to update to the most recent
+version of org-caldav.  See [[https://github.com/dengste/org-caldav/commit/618bf4cdc9be140ca1993901d017b7f18297f1b8][this org-caldav commit]] for more information.
+
 ** New and changed options
 *** New ~org-cite-natbib-export-bibliography~ option defining fallback bibliography style
 
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 81a77a770..06e90d032 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -526,25 +526,27 @@ (defun org-icalendar-cleanup-string (s)
 
 (defun org-icalendar-fold-string (s)
   "Fold string S according to RFC 5545."
-  (org-element-normalize-string
-   (mapconcat
-    (lambda (line)
-      ;; Limit each line to a maximum of 75 characters.  If it is
-      ;; longer, fold it by using "\r\n " as a continuation marker.
-      (let ((len (length line)))
-	(if (<= len 75) line
-	  (let ((folded-line (substring line 0 75))
-		(chunk-start 75)
-		chunk-end)
-	    ;; Since continuation marker takes up one character on the
-	    ;; line, real contents must be split at 74 chars.
-	    (while (< (setq chunk-end (+ chunk-start 74)) len)
-	      (setq folded-line
-		    (concat folded-line "\r\n "
-			    (substring line chunk-start chunk-end))
-		    chunk-start chunk-end))
-	    (concat folded-line "\r\n " (substring line chunk-start))))))
-    (org-split-string s "\n") "\r\n")))
+  (replace-regexp-in-string
+   "\n" "\r\n"
+   (org-element-normalize-string
+    (mapconcat
+     (lambda (line)
+       ;; Limit each line to a maximum of 75 characters.  If it is
+       ;; longer, fold it by using "\r\n " as a continuation marker.
+       (let ((len (length line)))
+	 (if (<= len 75) line
+	   (let ((folded-line (substring line 0 75))
+		 (chunk-start 75)
+		 chunk-end)
+	     ;; Since continuation marker takes up one character on the
+	     ;; line, real contents must be split at 74 chars.
+	     (while (< (setq chunk-end (+ chunk-start 74)) len)
+	       (setq folded-line
+		     (concat folded-line "\n "
+			     (substring line chunk-start chunk-end))
+		     chunk-start chunk-end))
+	     (concat folded-line "\n " (substring line chunk-start))))))
+     (org-split-string s "\n") "\n"))))
 
 
 \f
@@ -736,31 +738,30 @@ (\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are predefined, others
 should be treated as \"PRIVATE\" if they are unknown to the iCalendar server.
 
 Return VEVENT component as a string."
-  (org-icalendar-fold-string
-   (if (eq (org-element-property :type timestamp) 'diary)
-       (org-icalendar-transcode-diary-sexp
-	(org-element-property :raw-value timestamp) uid summary)
-     (concat "BEGIN:VEVENT\n"
-	     (org-icalendar-dtstamp) "\n"
-	     "UID:" uid "\n"
-	     (org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n"
-	     (org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
-	     ;; RRULE.
-	     (when (org-element-property :repeater-type timestamp)
-	       (format "RRULE:FREQ=%s;INTERVAL=%d\n"
-		       (cl-case (org-element-property :repeater-unit timestamp)
-			 (hour "HOURLY") (day "DAILY") (week "WEEKLY")
-			 (month "MONTHLY") (year "YEARLY"))
-		       (org-element-property :repeater-value timestamp)))
-	     "SUMMARY:" summary "\n"
-	     (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
-	     (and (org-string-nw-p class) (format "CLASS:%s\n" class))
-	     (and (org-string-nw-p description)
-		  (format "DESCRIPTION:%s\n" description))
-	     "CATEGORIES:" categories "\n"
-	     ;; VALARM.
-	     (org-icalendar--valarm entry timestamp summary)
-	     "END:VEVENT"))))
+  (if (eq (org-element-property :type timestamp) 'diary)
+      (org-icalendar-transcode-diary-sexp
+       (org-element-property :raw-value timestamp) uid summary)
+    (concat "BEGIN:VEVENT\n"
+	    (org-icalendar-dtstamp) "\n"
+	    "UID:" uid "\n"
+	    (org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n"
+	    (org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
+	    ;; RRULE.
+	    (when (org-element-property :repeater-type timestamp)
+	      (format "RRULE:FREQ=%s;INTERVAL=%d\n"
+		      (cl-case (org-element-property :repeater-unit timestamp)
+			(hour "HOURLY") (day "DAILY") (week "WEEKLY")
+			(month "MONTHLY") (year "YEARLY"))
+		      (org-element-property :repeater-value timestamp)))
+	    "SUMMARY:" summary "\n"
+	    (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
+	    (and (org-string-nw-p class) (format "CLASS:%s\n" class))
+	    (and (org-string-nw-p description)
+		 (format "DESCRIPTION:%s\n" description))
+	    "CATEGORIES:" categories "\n"
+	    ;; VALARM.
+	    (org-icalendar--valarm entry timestamp summary)
+	    "END:VEVENT")))
 
 (defun org-icalendar--vtodo
     (entry uid summary location description categories timezone class)
@@ -786,34 +787,33 @@ (defun org-icalendar--vtodo
 				 :day-start (nth 3 now)
 				 :month-start (nth 4 now)
 				 :year-start (nth 5 now)))))))
-    (org-icalendar-fold-string
-     (concat "BEGIN:VTODO\n"
-	     "UID:TODO-" uid "\n"
-	     (org-icalendar-dtstamp) "\n"
-	     (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n"
-	     (and (memq 'todo-due org-icalendar-use-deadline)
-		  (org-element-property :deadline entry)
-		  (concat (org-icalendar-convert-timestamp
-			   (org-element-property :deadline entry) "DUE" nil timezone)
-			  "\n"))
-	     "SUMMARY:" summary "\n"
-	     (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
-	     (and (org-string-nw-p class) (format "CLASS:%s\n" class))
-	     (and (org-string-nw-p description)
-		  (format "DESCRIPTION:%s\n" description))
-	     "CATEGORIES:" categories "\n"
-	     "SEQUENCE:1\n"
-	     (format "PRIORITY:%d\n"
-		     (let ((pri (or (org-element-property :priority entry)
-				    org-priority-default)))
-		       (floor (- 9 (* 8. (/ (float (- org-priority-lowest pri))
-					    (- org-priority-lowest
-					       org-priority-highest)))))))
-	     (format "STATUS:%s\n"
-		     (if (eq (org-element-property :todo-type entry) 'todo)
-			 "NEEDS-ACTION"
-		       "COMPLETED"))
-	     "END:VTODO"))))
+    (concat "BEGIN:VTODO\n"
+	    "UID:TODO-" uid "\n"
+	    (org-icalendar-dtstamp) "\n"
+	    (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n"
+	    (and (memq 'todo-due org-icalendar-use-deadline)
+		 (org-element-property :deadline entry)
+		 (concat (org-icalendar-convert-timestamp
+			  (org-element-property :deadline entry) "DUE" nil timezone)
+			 "\n"))
+	    "SUMMARY:" summary "\n"
+	    (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
+	    (and (org-string-nw-p class) (format "CLASS:%s\n" class))
+	    (and (org-string-nw-p description)
+		 (format "DESCRIPTION:%s\n" description))
+	    "CATEGORIES:" categories "\n"
+	    "SEQUENCE:1\n"
+	    (format "PRIORITY:%d\n"
+		    (let ((pri (or (org-element-property :priority entry)
+				   org-priority-default)))
+		      (floor (- 9 (* 8. (/ (float (- org-priority-lowest pri))
+					   (- org-priority-lowest
+					      org-priority-highest)))))))
+	    (format "STATUS:%s\n"
+		    (if (eq (org-element-property :todo-type entry) 'todo)
+			"NEEDS-ACTION"
+		      "COMPLETED"))
+	    "END:VTODO")))
 
 (defun org-icalendar--valarm (entry timestamp summary)
   "Create a VALARM component.
@@ -879,19 +879,20 @@ (defun org-icalendar--vcalendar (name owner tz description contents)
 NAME, OWNER, TZ, DESCRIPTION and CONTENTS are all strings giving,
 respectively, the name of the calendar, its owner, the timezone
 used, a short description and the other components included."
-  (concat (format "BEGIN:VCALENDAR
+  (org-icalendar-fold-string
+   (concat (format "BEGIN:VCALENDAR
 VERSION:2.0
 X-WR-CALNAME:%s
 PRODID:-//%s//Emacs with Org mode//EN
 X-WR-TIMEZONE:%s
 X-WR-CALDESC:%s
 CALSCALE:GREGORIAN\n"
-		  (org-icalendar-cleanup-string name)
-		  (org-icalendar-cleanup-string owner)
-		  (org-icalendar-cleanup-string tz)
-		  (org-icalendar-cleanup-string description))
-	  contents
-	  "END:VCALENDAR\n"))
+		   (org-icalendar-cleanup-string name)
+		   (org-icalendar-cleanup-string owner)
+		   (org-icalendar-cleanup-string tz)
+		   (org-icalendar-cleanup-string description))
+	   contents
+	   "END:VCALENDAR\n")))
 
 
 \f
diff --git a/testing/lisp/test-ox-icalendar.el b/testing/lisp/test-ox-icalendar.el
new file mode 100644
index 000000000..539d2a0e0
--- /dev/null
+++ b/testing/lisp/test-ox-icalendar.el
@@ -0,0 +1,46 @@
+;;; test-ox-icalendar.el --- tests for ox-icalendar.el  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2023  Jack Kamm
+
+;; Author: Jack Kamm <jackkamm@gmail.com>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests checking validity of Org iCalendar export output.
+
+;;; Code:
+
+(require 'ox-icalendar)
+
+(ert-deftest test-ox-icalendar/crfl-endings ()
+  "Test every line of iCalendar export has CRFL ending."
+  (should
+   (seq-every-p
+    (lambda (x) (equal (substring x -1) "\r"))
+    (org-split-string
+     (org-test-with-temp-text
+      "* Test event
+:PROPERTIES:
+:ID:       b17d8f92-1beb-442e-be4d-d2060fa3c7ff
+:END:
+<2023-03-30 Thu>"
+      (with-current-buffer
+          (org-export-to-buffer 'icalendar "*Test iCalendar Export*")
+        (buffer-string)))
+     "\n"))))
+
+(provide 'test-ox-icalendar)
+;;; test-ox-icalendar.el ends here
-- 
2.39.2


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-31  5:55   ` Jack Kamm
@ 2023-03-31 13:07     ` Ihor Radchenko
  2023-03-31 15:50       ` Jack Kamm
  0 siblings, 1 reply; 21+ messages in thread
From: Ihor Radchenko @ 2023-03-31 13:07 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> However you are right that other parts of the iCalendar export have
> inconsistent line endings. Currently, VEVENT and VTODO components have
> the correct CRLF endings, but the other parts of the VCALENDAR do not
> (such as the preamble).
>
> I like your suggestion in the above thread to just wrap the whole
> export in `org-icalendar-fold-string'.  Though I think it's slightly
> nicer to do it in `org-icalendar--vcalendar' instead of
> `org-icalendar-template'.
>
> So, I've attached a standalone patch to do this. It also fixes an issue
> with `org-icalendar-fold-string' where the last newline was missing
> "\r", and adds a unit test.

Thanks!
Note that I did not implement my suggestion because I am concerned if
putting CRLF is safe as every single line ending.

I am looking at
https://icalendar.org/iCalendar-RFC-5545/3-6-2-to-do-component.html, and
I note that only BEGIN:VTODO and END:VTODO lines must actually have
CRLF. For example,
https://icalendar.org/iCalendar-RFC-5545/3-3-11-text.html has no
mentions of CRLF, but does talk about escaping staff.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-31 13:07     ` Ihor Radchenko
@ 2023-03-31 15:50       ` Jack Kamm
  2023-03-31 17:51         ` Ihor Radchenko
  0 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-03-31 15:50 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

Ihor Radchenko <yantar92@posteo.net> writes:

> Thanks!
> Note that I did not implement my suggestion because I am concerned if
> putting CRLF is safe as every single line ending.
>
> I am looking at
> https://icalendar.org/iCalendar-RFC-5545/3-6-2-to-do-component.html, and
> I note that only BEGIN:VTODO and END:VTODO lines must actually have
> CRLF. For example,
> https://icalendar.org/iCalendar-RFC-5545/3-3-11-text.html has no
> mentions of CRLF, but does talk about escaping staff.

My reading of [1] is that all lines must end with CRLF:

> The iCalendar object is organized into individual lines of text,
> called content lines. Content lines are delimited by a line break,
> which is a CRLF sequence

And in particular, for the component properties after BEGIN:VTODO, [1]
gives the general notation as:

> contentline   = name *(";" param ) ":" value CRLF

For example, the DTSTART notation [2] is:

>  dtstart    = "DTSTART" dtstparam ":" dtstval CRLF

And the same is true for all the other properties.

[1] https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html
[2] https://icalendar.org/iCalendar-RFC-5545/3-8-2-4-date-time-start.html


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-31 15:50       ` Jack Kamm
@ 2023-03-31 17:51         ` Ihor Radchenko
  2023-03-31 22:20           ` Jack Kamm
  0 siblings, 1 reply; 21+ messages in thread
From: Ihor Radchenko @ 2023-03-31 17:51 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

>> I am looking at
>> https://icalendar.org/iCalendar-RFC-5545/3-6-2-to-do-component.html, and
>> I note that only BEGIN:VTODO and END:VTODO lines must actually have
>> CRLF. For example,
>> https://icalendar.org/iCalendar-RFC-5545/3-3-11-text.html has no
>> mentions of CRLF, but does talk about escaping staff.
>
> My reading of [1] is that all lines must end with CRLF:

Good to hear that we do not need to worry about the need to mix CRLF and
\n.

I now only have one minor concern about `org-icalendar-fold-string' when
the original buffer contains DOS line endings. May they mess things up
producing \r\r\n? If not, feel free to install the patch.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-31 17:51         ` Ihor Radchenko
@ 2023-03-31 22:20           ` Jack Kamm
  2023-04-01  8:30             ` Ihor Radchenko
  0 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-03-31 22:20 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

Ihor Radchenko <yantar92@posteo.net> writes:

> I now only have one minor concern about `org-icalendar-fold-string' when
> the original buffer contains DOS line endings. May they mess things up
> producing \r\r\n?

There are 2 issues here: what does `org-icalendar-fold-string' do when
string already contains \r, and what does `org-export-to-file' do when
`org-export-coding-system' or `buffer-file-coding-system' is dos-like.

In both cases, the patch doesn't change the existing behavior -- which
is to produce \r\r\n.

For issue 1, what `org-icalendar-fold-string' does when string already
contains \r\n, you can see that it produces \r\r\n as follows:

emacs -Q -l ox-icalendar
M-:
(org-icalendar-fold-string (org-icalendar-fold-string "Line1\nLine2"))

This is why the patch removes the calls to `org-icalendar-fold-string'
in `org-icalendar--vevent' and `org-icalendar--vtodo' -- otherwise we
would add \r multiple times to the same string.

To change this behavior of `org-icalendar-fold-string', we could
modify the patch to do:

 (defun org-icalendar-fold-string (s)
   "Fold string S according to RFC 5545."
   (replace-regexp-in-string
-   "\n" "\r\n"
+   "\r*\n" "\r\n"

which would strip out any extra \r at end of line. Another alternative
would be to use "\r?\n" instead of "\r*\n".

For the second issue -- when `org-export-coding-system' is dos (or
similar), the file created by `org-export-to-file' will contain
\r\r\n. This was already the pre-existing behavior, but note the patch
does cause a minor change here: before the patch just the main body
will have \r\r\n, but after the patch, the preamble will also have it.


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-31 22:20           ` Jack Kamm
@ 2023-04-01  8:30             ` Ihor Radchenko
  2023-04-02  0:47               ` Jack Kamm
  0 siblings, 1 reply; 21+ messages in thread
From: Ihor Radchenko @ 2023-04-01  8:30 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> For issue 1, what `org-icalendar-fold-string' does when string already
> contains \r\n, you can see that it produces \r\r\n as follows:
>
> emacs -Q -l ox-icalendar
> M-:
> (org-icalendar-fold-string (org-icalendar-fold-string "Line1\nLine2"))
>
> This is why the patch removes the calls to `org-icalendar-fold-string'
> in `org-icalendar--vevent' and `org-icalendar--vtodo' -- otherwise we
> would add \r multiple times to the same string.
>
> To change this behavior of `org-icalendar-fold-string', we could
> modify the patch to do:
>
>  (defun org-icalendar-fold-string (s)
>    "Fold string S according to RFC 5545."
>    (replace-regexp-in-string
> -   "\n" "\r\n"
> +   "\r*\n" "\r\n"
>
> which would strip out any extra \r at end of line. Another alternative
> would be to use "\r?\n" instead of "\r*\n".

"\r*\n" looks safer.

> For the second issue -- when `org-export-coding-system' is dos (or
> similar), the file created by `org-export-to-file' will contain
> \r\r\n. This was already the pre-existing behavior, but note the patch
> does cause a minor change here: before the patch just the main body
> will have \r\r\n, but after the patch, the preamble will also have it.

I see.
Looking at
https://icalendar.org/iCalendar-RFC-5545/3-1-4-character-set.html:

    There is not a property parameter to declare the charset used in a
    property value. The default charset for an iCalendar stream is UTF-8 as
    defined in [RFC3629].

So, we should probably override `org-export-coding-system', even when it
is set. iCalendar demands UTF8 anyway.

We likely want (according to 34.10.1 Basic Concepts of Coding Systems):

       The coding system ‘utf-8-emacs’ specifies that the data is
    represented in the internal Emacs encoding (*note Text
    Representations::).  This is like ‘raw-text’ in that no code conversion
    happens, but different in that the result is multibyte data.  The name
    ‘emacs-internal’ is an alias for ‘utf-8-emacs-unix’ (so it forces no
    conversion of end-of-line, unlike ‘utf-8-emacs’, which can decode all 3
    kinds of end-of-line conventions).

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-01  8:30             ` Ihor Radchenko
@ 2023-04-02  0:47               ` Jack Kamm
  2023-04-02  8:48                 ` Ihor Radchenko
  0 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-04-02  0:47 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

[-- Attachment #1: Type: text/plain, Size: 706 bytes --]

Ihor Radchenko <yantar92@posteo.net> writes:

> So, we should probably override `org-export-coding-system', even when it
> is set. iCalendar demands UTF8 anyway.

Also, ox-icalendar already sets ":ascii-charset utf-8" in the ext-plist
during export.

> We likely want (according to 34.10.1 Basic Concepts of Coding Systems):

I attach a new patch, which takes the approach of converting to
utf-8-dos in `org-icalendar-after-save-hook', instead of converting
newlines in `org-icalendar-fold-string'.

I think this way is simpler, and should be more robust across locales.

Note, this means the string returned by `org-export-as' won't contain
CRLF. Instead, the newlines are converted during post-process.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-ox-icalendar-Use-consistent-CRLF-line-endings.patch --]
[-- Type: text/x-patch, Size: 5093 bytes --]

From 04761429f82bfd2aee63f4978afec3449abaa37d Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sat, 1 Apr 2023 16:53:35 -0700
Subject: [PATCH] ox-icalendar: Use consistent CRLF line endings

Fixes issue where the ox-icalendar export uses an inconsistent mix of
dos and unix style line endings.

* lisp/ox-icalendar.el (org-icalendar-fold-string): No longer converts
to CRLF, instead delegating that to `org-icalendar--convert-eol'.
(org-icalendar--convert-eol): New function to convert EOL to CRLF. It
runs early in `org-icalendar-after-save-hook'.
* testing/lisp/test-ox-icalendar.el: New file for unit tests of
ox-icalendar.  Add an initial test for CRLF line endings.

See also:

https://list.orgmode.org/87o7oetneo.fsf@localhost/T/#m3e3eb80f9fc51ba75854b33ebfe9ecdefa2ded24

https://list.orgmode.org/orgmode/87ilgljv6i.fsf@localhost/
---
 etc/ORG-NEWS                      | 12 +++++++++
 lisp/ox-icalendar.el              | 14 +++++++---
 testing/lisp/test-ox-icalendar.el | 44 +++++++++++++++++++++++++++++++
 3 files changed, 67 insertions(+), 3 deletions(-)
 create mode 100644 testing/lisp/test-ox-icalendar.el

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index ac233a986..9f7d01707 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -23,6 +23,18 @@ If you still want to use python-mode with ob-python, you might
 consider [[https://gitlab.com/jackkamm/ob-python-mode-mode][ob-python-mode-mode]], where the code to support python-mode
 has been ported to.
 
+*** =ox-icalendar.el= line ending fix may affect downstream packages
+
+iCalendar export now uses dos-style CRLF ("\r\n") line endings
+throughout, as required by the iCalendar specification (RFC 5545).
+Previously, the export used an inconsistent mix of dos and unix line
+endings.
+
+This might cause errors in external packages that parse output from
+ox-icalendar.  In particular, older versions of org-caldav may
+encounter issues, and users are advised to update to the most recent
+version of org-caldav.  See [[https://github.com/dengste/org-caldav/commit/618bf4cdc9be140ca1993901d017b7f18297f1b8][this org-caldav commit]] for more information.
+
 ** New and changed options
 *** New ~org-cite-natbib-export-bibliography~ option defining fallback bibliography style
 
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 81a77a770..7f675b5d0 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -540,12 +540,20 @@ (defun org-icalendar-fold-string (s)
 	    ;; line, real contents must be split at 74 chars.
 	    (while (< (setq chunk-end (+ chunk-start 74)) len)
 	      (setq folded-line
-		    (concat folded-line "\r\n "
+		    (concat folded-line "\n "
 			    (substring line chunk-start chunk-end))
 		    chunk-start chunk-end))
-	    (concat folded-line "\r\n " (substring line chunk-start))))))
-    (org-split-string s "\n") "\r\n")))
+	    (concat folded-line "\n " (substring line chunk-start))))))
+    (org-split-string s "\n") "\n")))
 
+(defun org-icalendar--convert-eol (f)
+  "Convert line endings to CRLF as per RFC 5545."
+  (with-temp-buffer
+    (insert-file-contents f)
+    (let ((coding-system-for-write 'utf-8-dos))
+      (write-region nil nil f))))
+
+(add-hook 'org-icalendar-after-save-hook #'org-icalendar--convert-eol -90)
 
 \f
 ;;; Filters
diff --git a/testing/lisp/test-ox-icalendar.el b/testing/lisp/test-ox-icalendar.el
new file mode 100644
index 000000000..bfc756d51
--- /dev/null
+++ b/testing/lisp/test-ox-icalendar.el
@@ -0,0 +1,44 @@
+;;; test-ox-icalendar.el --- tests for ox-icalendar.el  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2023  Jack Kamm
+
+;; Author: Jack Kamm <jackkamm@gmail.com>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests checking validity of Org iCalendar export output.
+
+;;; Code:
+
+(require 'ox-icalendar)
+
+(ert-deftest test-ox-icalendar/crlf-endings ()
+  "Test every line of iCalendar export has CRLF ending."
+  (let ((tmp-ics (org-test-with-temp-text-in-file
+                  "* Test event
+:PROPERTIES:
+:ID:       b17d8f92-1beb-442e-be4d-d2060fa3c7ff
+:END:
+<2023-03-30 Thu>"
+                  (expand-file-name (org-icalendar-export-to-ics)))))
+    (unwind-protect
+        (with-temp-buffer
+          (insert-file-contents tmp-ics)
+          (should (eql 1 (coding-system-eol-type last-coding-system-used))))
+      (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(provide 'test-ox-icalendar)
+;;; test-ox-icalendar.el ends here
-- 
2.39.2


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-02  0:47               ` Jack Kamm
@ 2023-04-02  8:48                 ` Ihor Radchenko
  2023-04-02 15:34                   ` Jack Kamm
  0 siblings, 1 reply; 21+ messages in thread
From: Ihor Radchenko @ 2023-04-02  8:48 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

>> We likely want (according to 34.10.1 Basic Concepts of Coding Systems):
>
> I attach a new patch, which takes the approach of converting to
> utf-8-dos in `org-icalendar-after-save-hook', instead of converting
> newlines in `org-icalendar-fold-string'.
>
> I think this way is simpler, and should be more robust across locales.
>
> Note, this means the string returned by `org-export-as' won't contain
> CRLF. Instead, the newlines are converted during post-process.

Looks reasonable, but I have one comment on the code.

> +(add-hook 'org-icalendar-after-save-hook #'org-icalendar--convert-eol -90)

We should not use user-defined hooks for things that must be executed.
Imagine that a user customizes the hook after loading ox-icalendar and
removes the call to `org-icalendar--convert-eol'?

Instead, we should better explicitly call the necessary functions.
I was told this by Emacs devs.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-02  8:48                 ` Ihor Radchenko
@ 2023-04-02 15:34                   ` Jack Kamm
  2023-04-02 16:32                     ` Ihor Radchenko
  0 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-04-02 15:34 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

[-- Attachment #1: Type: text/plain, Size: 737 bytes --]

Ihor Radchenko <yantar92@posteo.net> writes:

> Looks reasonable, but I have one comment on the code.
> We should not use user-defined hooks for things that must be executed.
> Instead, we should better explicitly call the necessary functions.

Thanks, I updated the patch to explicitly call the function.

Also, I tweaked the coding-system-for-write to be a bit safer, in case
of edge cases where utf-8 doesn't work -- I think RFC 5545 just says
it's the default charset.

Attached is the (I think) final version of the patch. I'll install it
soon, unless I hear otherwise.

PS I haven't forgotten your feedback on the original VTODO-related
patches (thanks for that review). I'll work on that next, but it might
take me a bit longer.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-ox-icalendar-Use-consistent-CRLF-line-endings.patch --]
[-- Type: text/x-patch, Size: 6850 bytes --]

From aa59625cd08dcee767f42ad8d45d8902aa8d38bd Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sat, 1 Apr 2023 16:53:35 -0700
Subject: [PATCH] ox-icalendar: Use consistent CRLF line endings

Fixes issue where the ox-icalendar export uses an inconsistent mix of
dos and unix style line endings.

* lisp/ox-icalendar.el (org-icalendar-fold-string): No longer converts
to CRLF, instead delegating that to `org-icalendar--post-process-file'.
(org-icalendar--post-process-file): New function to handle exported
file post-processing.  Converts EOL to CRLF, and then runs
`org-icalendar-after-save-hook'.
(org-icalendar-export-to-ics, org-icalendar-export-current-agenda,
org-icalendar--combine-files): Call `org-icalendar--post-process-file'
instead of running `org-icalendar-after-save-hook' directly.
* testing/lisp/test-ox-icalendar.el: New file for unit tests of
ox-icalendar.  Add an initial test for CRLF line endings.

See also:

https://list.orgmode.org/87o7oetneo.fsf@localhost/T/#m3e3eb80f9fc51ba75854b33ebfe9ecdefa2ded24

https://list.orgmode.org/orgmode/87ilgljv6i.fsf@localhost/
---
 etc/ORG-NEWS                      | 12 +++++++++
 lisp/ox-icalendar.el              | 27 ++++++++++++-------
 testing/lisp/test-ox-icalendar.el | 44 +++++++++++++++++++++++++++++++
 3 files changed, 74 insertions(+), 9 deletions(-)
 create mode 100644 testing/lisp/test-ox-icalendar.el

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index ac233a986..9f7d01707 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -23,6 +23,18 @@ If you still want to use python-mode with ob-python, you might
 consider [[https://gitlab.com/jackkamm/ob-python-mode-mode][ob-python-mode-mode]], where the code to support python-mode
 has been ported to.
 
+*** =ox-icalendar.el= line ending fix may affect downstream packages
+
+iCalendar export now uses dos-style CRLF ("\r\n") line endings
+throughout, as required by the iCalendar specification (RFC 5545).
+Previously, the export used an inconsistent mix of dos and unix line
+endings.
+
+This might cause errors in external packages that parse output from
+ox-icalendar.  In particular, older versions of org-caldav may
+encounter issues, and users are advised to update to the most recent
+version of org-caldav.  See [[https://github.com/dengste/org-caldav/commit/618bf4cdc9be140ca1993901d017b7f18297f1b8][this org-caldav commit]] for more information.
+
 ** New and changed options
 *** New ~org-cite-natbib-export-bibliography~ option defining fallback bibliography style
 
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 81a77a770..ccc237721 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -540,12 +540,23 @@ (defun org-icalendar-fold-string (s)
 	    ;; line, real contents must be split at 74 chars.
 	    (while (< (setq chunk-end (+ chunk-start 74)) len)
 	      (setq folded-line
-		    (concat folded-line "\r\n "
+		    (concat folded-line "\n "
 			    (substring line chunk-start chunk-end))
 		    chunk-start chunk-end))
-	    (concat folded-line "\r\n " (substring line chunk-start))))))
-    (org-split-string s "\n") "\r\n")))
-
+	    (concat folded-line "\n " (substring line chunk-start))))))
+    (org-split-string s "\n") "\n")))
+
+(defun org-icalendar--post-process-file (file)
+  "Post-process the exported iCalendar FILE.
+Converts line endings to dos-style CRLF as per RFC 5545, then
+runs `org-icalendar-after-save-hook'."
+  (with-temp-buffer
+    (insert-file-contents file)
+    (let ((coding-system-for-write (coding-system-change-eol-conversion
+                                    last-coding-system-used 'dos)))
+      (write-region nil nil file)))
+  (run-hook-with-args 'org-icalendar-after-save-hook file)
+  nil)
 
 \f
 ;;; Filters
@@ -932,8 +943,7 @@ (defun org-icalendar-export-to-ics
     (org-export-to-file 'icalendar outfile
       async subtreep visible-only body-only
       '(:ascii-charset utf-8 :ascii-links-to-notes nil)
-      '(lambda (file)
-	 (run-hook-with-args 'org-icalendar-after-save-hook file) nil))))
+      #'org-icalendar--post-process-file)))
 
 ;;;###autoload
 (defun org-icalendar-export-agenda-files (&optional async)
@@ -1019,7 +1029,7 @@ (defun org-icalendar-export-current-agenda (file)
 	(or (org-string-nw-p org-icalendar-timezone) (format-time-string "%Z"))
 	org-icalendar-combined-description
 	contents)))
-    (run-hook-with-args 'org-icalendar-after-save-hook file)))
+    (org-icalendar--post-process-file file)))
 
 (defun org-icalendar--combine-files (&rest files)
   "Combine entries from multiple files into an iCalendar file.
@@ -1061,8 +1071,7 @@ (defun org-icalendar--combine-files (&rest files)
 	       (when (and org-icalendar-include-bbdb-anniversaries
 			  (require 'ol-bbdb nil t))
 		 (with-output-to-string (org-bbdb-anniv-export-ical)))))))
-	  (run-hook-with-args 'org-icalendar-after-save-hook
-			      org-icalendar-combined-agenda-file))
+	  (org-icalendar--post-process-file org-icalendar-combined-agenda-file))
       (org-release-buffers org-agenda-new-buffers))))
 
 
diff --git a/testing/lisp/test-ox-icalendar.el b/testing/lisp/test-ox-icalendar.el
new file mode 100644
index 000000000..bfc756d51
--- /dev/null
+++ b/testing/lisp/test-ox-icalendar.el
@@ -0,0 +1,44 @@
+;;; test-ox-icalendar.el --- tests for ox-icalendar.el  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2023  Jack Kamm
+
+;; Author: Jack Kamm <jackkamm@gmail.com>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests checking validity of Org iCalendar export output.
+
+;;; Code:
+
+(require 'ox-icalendar)
+
+(ert-deftest test-ox-icalendar/crlf-endings ()
+  "Test every line of iCalendar export has CRLF ending."
+  (let ((tmp-ics (org-test-with-temp-text-in-file
+                  "* Test event
+:PROPERTIES:
+:ID:       b17d8f92-1beb-442e-be4d-d2060fa3c7ff
+:END:
+<2023-03-30 Thu>"
+                  (expand-file-name (org-icalendar-export-to-ics)))))
+    (unwind-protect
+        (with-temp-buffer
+          (insert-file-contents tmp-ics)
+          (should (eql 1 (coding-system-eol-type last-coding-system-used))))
+      (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(provide 'test-ox-icalendar)
+;;; test-ox-icalendar.el ends here
-- 
2.39.2


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-02 15:34                   ` Jack Kamm
@ 2023-04-02 16:32                     ` Ihor Radchenko
  0 siblings, 0 replies; 21+ messages in thread
From: Ihor Radchenko @ 2023-04-02 16:32 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> Attached is the (I think) final version of the patch. I'll install it
> soon, unless I hear otherwise.

I have no further comments. Thanks!

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-27 11:59 ` Ihor Radchenko
  2023-03-31  5:55   ` Jack Kamm
@ 2023-04-14 16:57   ` Jack Kamm
  2023-04-14 18:46     ` Ihor Radchenko
  2023-06-11 15:35   ` [PATCH] " Jack Kamm
  2 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-04-14 16:57 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

Ihor Radchenko <yantar92@posteo.net> writes:

> Another scenario we may need to consider is when schedule has a repeater
> while deadline does not, and vice versa. The former scenario is probably
> valid - a VTODO with limited number of occurrences.

That is an interesting idea; and we can use the UNTIL or COUNT keywords
in RRULE to implement it.

However, it doesn't seem completely faithful to the way the TODO ends up
in the Org Agenda (or does Org have some option to use DEADLINE to bound
a repeating SCHEDULED in this way?)

I think the most faithful way to represent different SCHEDULED and
DEADLINE repeaters is to export 2 separate VTODOs, each with different
RRULE. Then the exported iCalendar will look just like the Org
Agenda. It is also in line with how ox-icalendar exports multiple
timestamps to separate VEVENTs.

That said, I am not really happy with this solution either. The fact
that ox-icalendar can create multiple VEVENT per entry already creates
headaches for any setup doing bidirectional sync between Org and
iCalendar, such as with org-caldav, ical2org.awk, or ical2orgpy. And I
am hesitant to make this problem worse, by making it happen for VTODO as
well.


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-14 16:57   ` Jack Kamm
@ 2023-04-14 18:46     ` Ihor Radchenko
  2023-04-15  3:13       ` Jack Kamm
  0 siblings, 1 reply; 21+ messages in thread
From: Ihor Radchenko @ 2023-04-14 18:46 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> Ihor Radchenko <yantar92@posteo.net> writes:
>
>> Another scenario we may need to consider is when schedule has a repeater
>> while deadline does not, and vice versa. The former scenario is probably
>> valid - a VTODO with limited number of occurrences.
>
> That is an interesting idea; and we can use the UNTIL or COUNT keywords
> in RRULE to implement it.
>
> However, it doesn't seem completely faithful to the way the TODO ends up
> in the Org Agenda (or does Org have some option to use DEADLINE to bound
> a repeating SCHEDULED in this way?)

DUE in iCalendar and DEADLINE in Org are not exactly the same. So, of
course, there is a room for ambiguity.

The question is: does iCalendar allow something like

DTSTART;TZID=America/New_York:19970105T083000
RRULE:FREQ=YEARLY
DUE;TZID=America/New_York:20070105T083000

and repeats past DUE?

If not, we have to choose when exporting from Org source - either to
keep DUE or not.

> I think the most faithful way to represent different SCHEDULED and
> DEADLINE repeaters is to export 2 separate VTODOs, each with different
> RRULE. Then the exported iCalendar will look just like the Org
> Agenda. It is also in line with how ox-icalendar exports multiple
> timestamps to separate VEVENTs.

If we want to leave as many options as possible to the users, we can (1)
Implement ICALENAR_DUE property that will set DUE explicitly on export;
(2) ICALENDAR_DUE may allow special values that will indicate how to
treat Org DEADLINEs - make them into DUE, use Org DEADLINE as a
bound for SCHEDULED repeater, or ignore DEADLINE completely.

> That said, I am not really happy with this solution either. The fact
> that ox-icalendar can create multiple VEVENT per entry already creates
> headaches for any setup doing bidirectional sync between Org and
> iCalendar, such as with org-caldav, ical2org.awk, or ical2orgpy. And I
> am hesitant to make this problem worse, by making it happen for VTODO as
> well.

Is there any reason for this? May we instead export to a single VEVENT
with appropriate RDATE list?

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-14 18:46     ` Ihor Radchenko
@ 2023-04-15  3:13       ` Jack Kamm
  2023-04-15  9:56         ` Ihor Radchenko
  0 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-04-15  3:13 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

Ihor Radchenko <yantar92@posteo.net> writes:

> The question is: does iCalendar allow something like
>
> DTSTART;TZID=America/New_York:19970105T083000
> RRULE:FREQ=YEARLY
> DUE;TZID=America/New_York:20070105T083000
>
> and repeats past DUE?
>
> If not, we have to choose when exporting from Org source - either to
> keep DUE or not.

From the defintion of RRULE [1]: 

      If the duration of the recurring component is specified with the
      "DTEND" or "DUE" property, then the same exact duration will apply
      to all the members of the generated recurrence set.

So the RRULE applies to both DTSTART and DUE, and the repeats continue
past DUE.

But, another thing to note from the definition of DTSTART [2]:

      This property [DTSTART] is REQUIRED in all types of recurring
      calendar components that specify the "RRULE" property.

So technically, a standalone DEADLINE + repeater isn't allowed -- a
repeating task must always have a start date.

But my impression is that not all iCalendar programs respect this. In
particular, Tasks.org app with Nextcloud server seemed to allow a
standalone repeating deadline. But I will check this more carefully, and
also in a couple more programs (radicale, Evolution).

But still, maybe we should stick to the requirement, and only export
repeater on SCHEDULED. That would simplify the implementation. The
downside is that repeating deadlines won't show up in iCalendar, which
seems undesirable.

[1] https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.3:
[2] https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.4

> If we want to leave as many options as possible to the users, we can (1)
> Implement ICALENAR_DUE property that will set DUE explicitly on export;
> (2) ICALENDAR_DUE may allow special values that will indicate how to
> treat Org DEADLINEs - make them into DUE, use Org DEADLINE as a
> bound for SCHEDULED repeater, or ignore DEADLINE completely.

A couple of these behaviors can already be achieved by customizing
`org-icalendar-use-deadline' (making DUE or ignoring). For using
DEADLINE as a bound, we could potentially add another option for that.

> Is there any reason for this? May we instead export to a single VEVENT
> with appropriate RDATE list?

I guess if there are multiple timestamps with repeaters, it's easier to
export these as separate VEVENT, because it's not possible to have
multiple RRULE in one VEVENT.

But, your suggestion earlier in thread could also solve this: in case of
different repeaters, we can use RDATE to generate occurrences manually
sufficiently far into future (with defcustom for "how far").


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-15  3:13       ` Jack Kamm
@ 2023-04-15  9:56         ` Ihor Radchenko
  2023-04-16 17:19           ` Jack Kamm
  0 siblings, 1 reply; 21+ messages in thread
From: Ihor Radchenko @ 2023-04-15  9:56 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> So the RRULE applies to both DTSTART and DUE, and the repeats continue
> past DUE.
>
> But, another thing to note from the definition of DTSTART [2]:
>
>       This property [DTSTART] is REQUIRED in all types of recurring
>       calendar components that specify the "RRULE" property.
>
> So technically, a standalone DEADLINE + repeater isn't allowed -- a
> repeating task must always have a start date.

May we then use org-deadline-warning-days/timestamp warntime spec as DTSTART?
VALARM component is not fitting for warning days anyway.

> But still, maybe we should stick to the requirement, and only export
> repeater on SCHEDULED. That would simplify the implementation. The
> downside is that repeating deadlines won't show up in iCalendar, which
> seems undesirable.

Agree. We should better stick to the spec.

>> If we want to leave as many options as possible to the users, we can (1)
>> Implement ICALENAR_DUE property that will set DUE explicitly on export;
>> (2) ICALENDAR_DUE may allow special values that will indicate how to
>> treat Org DEADLINEs - make them into DUE, use Org DEADLINE as a
>> bound for SCHEDULED repeater, or ignore DEADLINE completely.
>
> A couple of these behaviors can already be achieved by customizing
> `org-icalendar-use-deadline' (making DUE or ignoring). For using
> DEADLINE as a bound, we could potentially add another option for that.

Yup. That's what I meant.

>> Is there any reason for this? May we instead export to a single VEVENT
>> with appropriate RDATE list?
>
> I guess if there are multiple timestamps with repeaters, it's easier to
> export these as separate VEVENT, because it's not possible to have
> multiple RRULE in one VEVENT.
>
> But, your suggestion earlier in thread could also solve this: in case of
> different repeaters, we can use RDATE to generate occurrences manually
> sufficiently far into future (with defcustom for "how far").

RDATE is exactly what I had in mind here.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [RFC] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-04-15  9:56         ` Ihor Radchenko
@ 2023-04-16 17:19           ` Jack Kamm
  0 siblings, 0 replies; 21+ messages in thread
From: Jack Kamm @ 2023-04-16 17:19 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

Ihor Radchenko <yantar92@posteo.net> writes:

>> So technically, a standalone DEADLINE + repeater isn't allowed -- a
>> repeating task must always have a start date.
>
> May we then use org-deadline-warning-days/timestamp warntime spec as DTSTART?
> VALARM component is not fitting for warning days anyway.
>
>> But still, maybe we should stick to the requirement, and only export
>> repeater on SCHEDULED. That would simplify the implementation. The
>> downside is that repeating deadlines won't show up in iCalendar, which
>> seems undesirable.
>
> Agree. We should better stick to the spec.

I took a closer look into how other programs handle RRULE, DTSTART, DUE.

I tried the following CalDav servers:

Nextcloud, radicale

And the following clients:

Tasks.org, Thunderbird, Evolution. (I did not use Nextcloud client
because it doesn't support repeating tasks, even though the Nextcloud
server does).

Thunderbird and Evolution clients do not allow creating repeating tasks
without start date -- if you try to do so, they will force you to
specify one.

Tasks.org client does allow repeating tasks with only a deadline (no
start date).  Nextcloud and radicale servers happily accept the
repeating deadline from Tasks.org without start date. When I download
the ICS file from the server, the VTODO contains RRULE and DUE, but not
DTSTART. When I validate the ICS file with icalendar.org [1], it accepts
the ICS as valid, even though it seemingly violates the spec by missing
DTSTART.

So, it seems there is some inconsistency about this in the iCalendar
ecosystem.

I have not yet reached a firm conclusion on the best solution, but am
leaning towards your suggestion to use org-deadline-warning-days for
DTSTART in this case. I'll try to have a more concrete, updated patch on
this ready in a couple weeks or so.


[1] https://icalendar.org/validator.html


^ permalink raw reply	[flat|nested] 21+ messages in thread

* [PATCH] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-03-27 11:59 ` Ihor Radchenko
  2023-03-31  5:55   ` Jack Kamm
  2023-04-14 16:57   ` Jack Kamm
@ 2023-06-11 15:35   ` Jack Kamm
  2023-06-12 10:36     ` Ihor Radchenko
  2 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-06-11 15:35 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

[-- Attachment #1: Type: text/plain, Size: 1770 bytes --]

Hello,

I am attaching an updated patch for ox-icalendar unscheduled and
repeating TODOs, incorporating some of Ihor's feedback to my RFC some
months ago.

Compared to my original RFC, here are the main changes:

- For unscheduled TODOs with repeating deadline, the deadline warning
  days is used as the start time by default, in order to comply with
  the iCalendar spec which demands a start time in this case.

- Previously I had separate patches for unscheduled and repeating
  TODOs, but now I combine them into a single patch because of the way
  repeats and start times are intertwined for repeating deadlines.

- New customization `org-icalendar-todo-unscheduled-start' controls
  the exported start time for unscheduled TODOs. It replaces
  `org-icalendar-todo-force-scheduling' from my previous version of
  the patch.

- In case of a SCHEDULED repeater, and a DEADLINE with no repeater,
  the task repeats until the deadline, using the RRULE UNTIL keyword.

- Added linting for the case where SCHEDULED and DEADLINE have
  mismatching repeaters.

- Added several tests for ox-icalendar, and a test for the new lint as well.

There are still a few cases that are not yet handled, but they are
less common and will take some more work to implement, so I would
prefer to leave them to future patches:

- Case where SCHEDULED and DEADLINE have mismatched repeaters.  We can
  use RDATE with differing DURATION for this.

- Case where DEADLINE has repeater but SCHEDULED does not.  We can use
  RDATE for the first instance, and RRULE for the subsequent repeats.

- Case of catch-up "++" repeaters.  We can use EXDATE to exclude
  repeats before today.

- Case of restart ".+" repeaters.  I don't think iCalendar can handle
  this case, and we should ignore it.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-ox-icalendar-Add-support-for-unscheduled-and-repeati.patch --]
[-- Type: text/x-patch, Size: 22707 bytes --]

From 1135e3e7cb08353892c439b085d3bf0bf1072ecb Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sun, 11 Jun 2023 07:50:20 -0700
Subject: [PATCH] ox-icalendar: Add support for unscheduled and repeating TODOs

* lisp/ox-icalendar.el (org-icalendar-todo-unscheduled-start): New
customization to control the exported start time of unscheduled tasks.
(org-icalendar--rrule): Helper function for RRULE export.
(org-icalendar--vevent): Use the new helper function for RRULE.
(org-icalendar--vtodo): Change how unscheduled TODOs are handled using
the new customization option.  Export SCHEDULED and DEADLINE
repeaters.  In case of SCHEDULED repeater and a DEADLINE without
repeater, treat DEADLINE as RRULE UNTIL.  Emit a warning for tricky
edge cases that are not yet implemented.
* testing/lisp/test-ox-icalendar.el
(test-ox-icalendar/todo-repeater-shared): Test for exporting shared
SCHEDULED/DEADLINE repeater.
(test-ox-icalendar/todo-repeating-deadline-warndays): Test using
warning days as DTSTART of repeating deadline.
(test-ox-icalendar/todo-repeater-until): Test using DEADLINE as RRULE
UNTIL.
(test-ox-icalendar/todo-repeater-until-utc): Test RRULE UNTIL is in
UTC format when DTSTART is not in local time format.
* lisp/org-lint.el (org-lint-mismatched-planning-repeaters): Add lint
for mismatched SCHEDULED and DEADLINE repeaters.
* testing/lisp/test-org-lint.el
(test-org-lint/mismatched-planning-repeaters): Add test for linting of
mismatched SCHEDULED and DEADLINE repeaters.
---
 etc/ORG-NEWS                      |  64 ++++++++++++
 lisp/org-lint.el                  |  34 ++++++
 lisp/ox-icalendar.el              | 165 +++++++++++++++++++++++++-----
 testing/lisp/test-org-lint.el     |   7 ++
 testing/lisp/test-ox-icalendar.el |  74 ++++++++++++++
 5 files changed, 320 insertions(+), 24 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 7e7015064..a24caddfe 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -50,6 +50,21 @@ ox-icalendar.  In particular, older versions of org-caldav may
 encounter issues, and users are advised to update to the most recent
 version of org-caldav.  See [[https://github.com/dengste/org-caldav/commit/618bf4cdc9be140ca1993901d017b7f18297f1b8][this org-caldav commit]] for more information.
 
+*** Icalendar export of unscheduled TODOs no longer have start time of today
+
+For TODOs without a scheduled start time, ox-icalendar no longer
+forces them to have a scheduled start time of today when exporting.
+
+Instead, the new customization ~org-icalendar-todo-unscheduled-start~
+controls the exported start date for unscheduled tasks.  Its default
+is ~recurring-deadline-warning~ which will export unscheduled tasks
+with no start date, unless it has a recurring deadline (in which case
+the iCalendar spec demands a start date, and
+~org-deadline-warning-days~ is used for that).
+
+To revert to the old behavior, set
+~org-icalendar-todo-unscheduled-start~ to ~current-datetime~.
+
 ** New and changed options
 *** Commands affected by ~org-fold-catch-invisible-edits~ can now be customized
 
@@ -188,6 +203,28 @@ default settings of "Body only", "Visible only", and "Force
 publishing" in the ~org-export-dispatch~ UI to be customized,
 respectively.
 
+*** New option ~org-icalendar-todo-unscheduled-start~ to control unscheduled TODOs in ox-icalendar
+
+~org-icalendar-todo-unscheduled-start~ controls how ox-icalendar
+exports the starting datetime for unscheduled TODOs.  Note this option
+only has an effect when ~org-icalendar-include-todo~ is non-nil.
+
+By default, ox-icalendar will not export a start datetime for
+unscheduled TODOs, except in cases where the iCalendar spec demands a
+start (specifically, for recurring deadlines, in which case
+~org-deadline-warning-days~ is used).
+
+Currently implemented options are:
+
+- ~recurring-deadline-warning~: The default as described above.
+- ~deadline-warning~: Use ~org-deadline-warning-days~ to set the start
+  time if the unscheduled task has a deadline (recurring or not).
+- ~current-datetime~: Revert to old behavior, using the current
+  datetime as the start of unscheduled tasks.
+- ~nil~: Never add a start time for unscheduled tasks.  For repeating
+  tasks this technically violates the iCalendar spec, but some
+  iCalendar programs support this usage.
+
 ** New features
 *** ~org-insert-todo-heading-respect-content~ now accepts prefix arguments
 
@@ -230,6 +267,33 @@ editing with Emacs while a ~:session~ block executes.
 When ~org-return-follows-link~ is non-nil and cursor is over an
 org-cite citation, ~org-return~ will call ~org-open-at-point~.
 
+*** Add support for repeating tasks in iCalendar export
+
+Repeating Scheduled and Deadline timestamps in TODOs are now exported
+as recurring tasks in iCalendar export.
+
+In case the TODO has just a single planning timestamp (Scheduled or
+Deadline, but not both), its repeater is used as the iCalendar
+recurrence rule (RRULE).
+
+If the TODO has both Scheduled and Deadline planning timestamps, then
+the following cases are implemented:
+
+- If both have the same repeater, then it is used as the RRULE.
+- Scheduled has repeater but Deadline does not: the Scheduled repeater
+  is used as RRULE, and Deadline is used as UNTIL (the end date for
+  the repeater). This is similar to ~repeated-after-deadline~ in
+  ~org-agenda-skip-scheduled-if-deadline-is-shown~.
+
+The following 2 cases are not yet implemented, and the repeater is
+skipped (with a warning) if the ox-icalendar export encounters them:
+
+- Deadline has a repeater but Scheduled does not.
+- Scheduled and Deadline have different repeaters.
+
+Also note that only vanilla repeaters are currently exported; the
+special repeaters ~++~ and ~.+~ are skipped.
+
 ** Miscellaneous
 *** =org-crypt.el= now applies initial visibility settings to decrypted entries
 
diff --git a/lisp/org-lint.el b/lisp/org-lint.el
index c2ed007ab..bec1340c5 100644
--- a/lisp/org-lint.el
+++ b/lisp/org-lint.el
@@ -70,6 +70,7 @@
 ;; - non-footnote definitions in footnote section,
 ;; - probable invalid keywords,
 ;; - invalid blocks,
+;; - mismatched repeaters in planning info line,
 ;; - misplaced planning info line,
 ;; - probable incomplete drawers,
 ;; - probable indented diary-sexps,
@@ -882,6 +883,34 @@ (defun org-lint-colon-in-name (ast)
 		    "Name \"%s\" contains a colon; Babel cannot use it as input"
 		    name)))))))
 
+(defun org-lint-mismatched-planning-repeaters (ast)
+  (org-element-map ast 'planning
+    (lambda (e)
+      (let* ((scheduled (org-element-property :scheduled e))
+             (deadline (org-element-property :deadline e))
+             (scheduled-repeater-type (org-element-property
+                                       :repeater-type scheduled))
+             (deadline-repeater-type (org-element-property
+                                      :repeater-type deadline))
+             (scheduled-repeater-value (org-element-property
+                                        :repeater-value scheduled))
+             (deadline-repeater-value (org-element-property
+                                       :repeater-value deadline)))
+        (when (and scheduled deadline
+                   (memq scheduled-repeater-type '(cumulate catch-up))
+                   (memq deadline-repeater-type '(cumulate catch-up))
+                   (> scheduled-repeater-value 0)
+                   (> deadline-repeater-value 0)
+                   (not
+                    (and
+                     (eq scheduled-repeater-type deadline-repeater-type)
+                     (eq (org-element-property :repeater-unit scheduled)
+                         (org-element-property :repeater-unit deadline))
+                     (eql scheduled-repeater-value deadline-repeater-value))))
+          (list
+           (org-element-property :begin e)
+           "Different repeaters in SCHEDULED and DEADLINE timestamps."))))))
+
 (defun org-lint-misplaced-planning-info (_)
   (let ((case-fold-search t)
 	reports)
@@ -1488,6 +1517,11 @@ (org-lint-add-checker 'invalid-block
   #'org-lint-invalid-block
   :trust 'low)
 
+(org-lint-add-checker 'mismatched-planning-repeaters
+  "Report mismatched repeaters in planning info line"
+  #'org-lint-mismatched-planning-repeaters
+  :trust 'low)
+
 (org-lint-add-checker 'misplaced-planning-info
   "Report misplaced planning info line"
   #'org-lint-misplaced-planning-info
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 163b3b983..8c569752b 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -231,6 +231,38 @@ (defcustom org-icalendar-include-todo nil
 	  (repeat :tag "Specific TODO keywords"
 		  (string :tag "Keyword"))))
 
+(defcustom org-icalendar-todo-unscheduled-start 'recurring-deadline-warning
+  "Exported start date of unscheduled TODOs.
+
+If `org-icalendar-use-scheduled' contains `todo-start' and a task
+has a \"SCHEDULED\" timestamp, that is always used as the start
+date.  Otherwise, this variable controls whether a start date is
+exported and what its value is.
+
+Note that the iCalendar spec RFC 5545 does not generally require
+tasks to have a start date, except for repeating tasks which do
+require a start date.  However some iCalendar programs ignore the
+requirement for repeating tasks, and allow repeating deadlines
+without a matching start date.
+
+This variable has no effect when `org-icalendar-include-todo' is nil.
+
+Valid values are:
+`recurring-deadline-warning'  If deadline repeater present,
+                              use `org-deadline-warning-days' as start.
+`deadline-warning'            If deadline present,
+                              use `org-deadline-warning-days' as start.
+`current-datetime'            Use the current date-time as start.
+nil                           Never add a start time for unscheduled tasks."
+  :group 'org-export-icalendar
+  :type '(choice
+	  (const :tag "Warning days if deadline recurring" recurring-deadline-warning)
+	  (const :tag "Warning days if deadline present" deadline-warning)
+	  (const :tag "Now" current-datetime)
+	  (const :tag "No start date" nil))
+  :package-version '(Org . "9.7")
+  :safe #'symbolp)
+
 (defcustom org-icalendar-include-bbdb-anniversaries nil
   "Non-nil means a combined iCalendar file should include anniversaries.
 The anniversaries are defined in the BBDB database."
@@ -731,6 +763,13 @@ (defun org-icalendar-entry (entry contents info)
        ;; Don't forget components from inner entries.
        contents))))
 
+(defun org-icalendar--rrule (unit value)
+  (format "RRULE:FREQ=%s;INTERVAL=%d"
+	  (cl-case unit
+	    (hour "HOURLY") (day "DAILY") (week "WEEKLY")
+	    (month "MONTHLY") (year "YEARLY"))
+	  value))
+
 (defun org-icalendar--vevent
     (entry timestamp uid summary location description categories timezone class)
   "Create a VEVENT component.
@@ -756,12 +795,11 @@ (\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are predefined, others
 	    (org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n"
 	    (org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
 	    ;; RRULE.
-	    (when (org-element-property :repeater-type timestamp)
-	      (format "RRULE:FREQ=%s;INTERVAL=%d\n"
-		      (cl-case (org-element-property :repeater-unit timestamp)
-			(hour "HOURLY") (day "DAILY") (week "WEEKLY")
-			(month "MONTHLY") (year "YEARLY"))
-		      (org-element-property :repeater-value timestamp)))
+            (when (org-element-property :repeater-type timestamp)
+              (concat (org-icalendar--rrule
+                       (org-element-property :repeater-unit timestamp)
+                       (org-element-property :repeater-value timestamp))
+                      "\n"))
 	    "SUMMARY:" summary "\n"
 	    (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
 	    (and (org-string-nw-p class) (format "CLASS:%s\n" class))
@@ -784,27 +822,106 @@ (defun org-icalendar--vtodo
 TIMEZONE specifies a time zone for this TODO only.
 
 Return VTODO component as a string."
-  (let ((start (or (and (memq 'todo-start org-icalendar-use-scheduled)
-			(org-element-property :scheduled entry))
-		   ;; If we can't use a scheduled time for some
-		   ;; reason, start task now.
-		   (let ((now (decode-time)))
-		     (list 'timestamp
-			   (list :type 'active
-				 :minute-start (nth 1 now)
-				 :hour-start (nth 2 now)
-				 :day-start (nth 3 now)
-				 :month-start (nth 4 now)
-				 :year-start (nth 5 now)))))))
+  (let* ((sc (and (memq 'todo-start org-icalendar-use-scheduled)
+		  (org-element-property :scheduled entry)))
+         (dl (and (memq 'todo-due org-icalendar-use-deadline)
+                  (org-element-property :deadline entry)))
+         ;; TODO Implement catch-up repeaters using EXDATE
+         (sc-repeat-p (and (eq (org-element-property :repeater-type sc)
+                               'cumulate)
+                           (> (org-element-property :repeater-value sc) 0)))
+         (dl-repeat-p (and (eq (org-element-property :repeater-type dl)
+                               'cumulate)
+                           (> (org-element-property :repeater-value dl) 0)))
+         (repeat-value (or (org-element-property :repeater-value sc)
+                           (org-element-property :repeater-value dl)))
+         (repeat-unit (or (org-element-property :repeater-unit sc)
+                          (org-element-property :repeater-unit dl)))
+         (repeat-until (and sc-repeat-p (not dl-repeat-p) dl))
+         (start
+          (cond
+           (sc)
+           ((eq org-icalendar-todo-unscheduled-start 'current-datetime)
+            (let ((now (decode-time)))
+	      (list 'timestamp
+		    (list :type 'active
+			  :minute-start (nth 1 now)
+			  :hour-start (nth 2 now)
+			  :day-start (nth 3 now)
+			  :month-start (nth 4 now)
+			  :year-start (nth 5 now)))))
+           ((or (and (eq org-icalendar-todo-unscheduled-start
+                         'deadline-warning)
+                     dl)
+                (and (eq org-icalendar-todo-unscheduled-start
+                         'recurring-deadline-warning)
+                     dl-repeat-p))
+            (let ((dl-raw (org-element-property :raw-value dl)))
+              (with-temp-buffer
+	        (insert dl-raw)
+                (goto-char (point-min))
+	        (org-timestamp-down-day (org-get-wdays dl-raw))
+	        (org-element-timestamp-parser)))))))
     (concat "BEGIN:VTODO\n"
 	    "UID:TODO-" uid "\n"
 	    (org-icalendar-dtstamp) "\n"
-	    (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n"
-	    (and (memq 'todo-due org-icalendar-use-deadline)
-		 (org-element-property :deadline entry)
-		 (concat (org-icalendar-convert-timestamp
-			  (org-element-property :deadline entry) "DUE" nil timezone)
-			 "\n"))
+            (when start (concat (org-icalendar-convert-timestamp
+                                 start "DTSTART" nil timezone)
+                                "\n"))
+	    (when (and dl (not repeat-until))
+	      (concat (org-icalendar-convert-timestamp
+		       dl "DUE" nil timezone)
+		      "\n"))
+            ;; RRULE
+            (cond
+             ;; SCHEDULED, DEADLINE have different repeaters
+             ((and dl-repeat-p
+                   (not (and (eq repeat-value (org-element-property
+                                               :repeater-value dl))
+                             (eq repeat-unit (org-element-property
+                                              :repeater-unit dl)))))
+              ;; TODO Implement via RDATE with changing DURATION
+              (warn "Not yet implemented: \
+different repeaters on SCHEDULED and DEADLINE. Skipping.")
+              nil)
+             ;; DEADLINE has repeater but SCHEDULED doesn't
+             ((and dl-repeat-p (and sc (not sc-repeat-p)))
+              ;; TODO SCHEDULED should only apply to first instance;
+              ;; use RDATE with custom DURATION to implement that
+              (warn "Not yet implemented: \
+repeater on DEADLINE but not SCHEDULED. Skipping.")
+              nil)
+             ((or sc-repeat-p dl-repeat-p)
+              (concat
+               (org-icalendar--rrule repeat-unit repeat-value)
+               ;; add UNTIL part to RRULE
+               (when repeat-until
+                 (let* ((start-time
+                         (org-element-property :minute-start start))
+                        ;; RFC5545 requires UTC iff DTSTART is not local time
+                        (local-time-p
+                         (and (not timezone)
+                              (equal org-icalendar-date-time-format
+                                     ":%Y%m%dT%H%M%S")))
+                        (encoded
+                         (org-encode-time
+                          0
+                          (or (org-element-property :minute-start repeat-until)
+                              0)
+                          (or (org-element-property :hour-start repeat-until)
+                              0)
+                          (org-element-property :day-start repeat-until)
+                          (org-element-property :month-start repeat-until)
+                          (org-element-property :year-start repeat-until))))
+                   (concat ";UNTIL="
+                           (cond
+                            ((not start-time)
+                             (format-time-string "%Y%m%d" encoded))
+                            (local-time-p
+                             (format-time-string "%Y%m%dT%H%M%S" encoded))
+                            ((format-time-string "%Y%m%dT%H%M%SZ"
+                                                 encoded t))))))
+               "\n")))
 	    "SUMMARY:" summary "\n"
 	    (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
 	    (and (org-string-nw-p class) (format "CLASS:%s\n" class))
diff --git a/testing/lisp/test-org-lint.el b/testing/lisp/test-org-lint.el
index 6ee1b1fab..f61b8647c 100644
--- a/testing/lisp/test-org-lint.el
+++ b/testing/lisp/test-org-lint.el
@@ -406,6 +406,13 @@ (ert-deftest test-org-lint/colon-in-name ()
    (org-test-with-temp-text "#+name: name\n| a |"
      (org-lint '(colon-in-name)))))
 
+(ert-deftest test-org-lint/mismatched-planning-repeaters ()
+  "Test `org-lint-mismatched-planning-repeaters' checker."
+  (should
+   (org-test-with-temp-text "* H
+DEADLINE: <2023-03-26 Sun +2w> SCHEDULED: <2023-03-26 Sun +1w>"
+     (org-lint '(mismatched-planning-repeaters)))))
+
 (ert-deftest test-org-lint/misplaced-planning-info ()
   "Test `org-lint-misplaced-planning-info' checker."
   (should
diff --git a/testing/lisp/test-ox-icalendar.el b/testing/lisp/test-ox-icalendar.el
index bfc756d51..6a0c961d7 100644
--- a/testing/lisp/test-ox-icalendar.el
+++ b/testing/lisp/test-ox-icalendar.el
@@ -40,5 +40,79 @@ (ert-deftest test-ox-icalendar/crlf-endings ()
           (should (eql 1 (coding-system-eol-type last-coding-system-used))))
       (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
 
+(ert-deftest test-ox-icalendar/todo-repeater-shared ()
+  "Test shared repeater on todo scheduled and deadline."
+  (let* ((org-icalendar-include-todo 'all)
+         (tmp-ics (org-test-with-temp-text-in-file
+                   "* TODO Both repeating
+DEADLINE: <2023-04-02 Sun +1m> SCHEDULED: <2023-03-26 Sun +1m>"
+                   (expand-file-name (org-icalendar-export-to-ics)))))
+    (unwind-protect
+        (with-temp-buffer
+          (insert-file-contents tmp-ics)
+          (save-excursion
+            (should (search-forward "DTSTART;VALUE=DATE:20230326")))
+          (save-excursion
+            (should (search-forward "DUE;VALUE=DATE:20230402")))
+          (save-excursion
+            (should (search-forward "RRULE:FREQ=MONTHLY;INTERVAL=1"))))
+      (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(ert-deftest test-ox-icalendar/todo-repeating-deadline-warndays ()
+  "Test repeating deadline with DTSTART as warning days."
+  (let* ((org-icalendar-include-todo 'all)
+         (org-icalendar-todo-unscheduled-start 'recurring-deadline-warning)
+         (tmp-ics (org-test-with-temp-text-in-file
+                   "* TODO Repeating deadline
+DEADLINE: <2023-04-02 Sun +2w -3d>"
+                   (expand-file-name (org-icalendar-export-to-ics)))))
+    (unwind-protect
+        (with-temp-buffer
+          (insert-file-contents tmp-ics)
+          (save-excursion
+            (should (search-forward "DTSTART;VALUE=DATE:20230330")))
+          (save-excursion
+            (should (search-forward "DUE;VALUE=DATE:20230402")))
+          (save-excursion
+            (should (search-forward "RRULE:FREQ=WEEKLY;INTERVAL=2"))))
+      (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(ert-deftest test-ox-icalendar/todo-repeater-until ()
+  "Test repeater on todo scheduled until deadline."
+  (let* ((org-icalendar-include-todo 'all)
+         (tmp-ics (org-test-with-temp-text-in-file
+                   "* TODO Repeating scheduled with nonrepeating deadline
+DEADLINE: <2023-05-01 Mon> SCHEDULED: <2023-03-26 Sun +3d>"
+                   (expand-file-name (org-icalendar-export-to-ics)))))
+    (unwind-protect
+        (with-temp-buffer
+          (insert-file-contents tmp-ics)
+          (save-excursion
+            (should (search-forward "DTSTART;VALUE=DATE:20230326")))
+          (save-excursion
+            (should (not (re-search-forward "^DUE" nil t))))
+          (save-excursion
+            (should (search-forward "RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=20230501"))))
+      (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(ert-deftest test-ox-icalendar/todo-repeater-until-utc ()
+  "Test that UNTIL is in UTC when DTSTART is not in local time format."
+  (let* ((org-icalendar-include-todo 'all)
+         (org-icalendar-date-time-format ":%Y%m%dT%H%M%SZ")
+         (tmp-ics (org-test-with-temp-text-in-file
+                   "* TODO Repeating scheduled with nonrepeating deadline
+DEADLINE: <2023-05-02 Tue> SCHEDULED: <2023-03-26 Sun 15:00 +3d>"
+                   (expand-file-name (org-icalendar-export-to-ics)))))
+    (unwind-protect
+        (with-temp-buffer
+          (insert-file-contents tmp-ics)
+          (save-excursion
+            (should (re-search-forward "DTSTART:2023032.T..0000")))
+          (save-excursion
+            (should (not (re-search-forward "^DUE" nil t))))
+          (save-excursion
+            (should (re-search-forward "RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=2023050.T..0000Z"))))
+      (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
 (provide 'test-ox-icalendar)
 ;;; test-ox-icalendar.el ends here
-- 
2.40.1


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* Re: [PATCH] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-06-11 15:35   ` [PATCH] " Jack Kamm
@ 2023-06-12 10:36     ` Ihor Radchenko
  2023-06-17 17:32       ` Jack Kamm
  0 siblings, 1 reply; 21+ messages in thread
From: Ihor Radchenko @ 2023-06-12 10:36 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> I am attaching an updated patch for ox-icalendar unscheduled and
> repeating TODOs, incorporating some of Ihor's feedback to my RFC some
> months ago.

Thanks! See some comments below.

> +*** New option ~org-icalendar-todo-unscheduled-start~ to control unscheduled TODOs in ox-icalendar
> +*** Add support for repeating tasks in iCalendar export
> ...
> +Also note that only vanilla repeaters are currently exported; the
> +special repeaters ~++~ and ~.+~ are skipped.

Would it make sense to throw a warning instead of silently skipping ~++~
and ~.+~ repeaters?

> +(defcustom org-icalendar-todo-unscheduled-start 'recurring-deadline-warning
> +  "Exported start date of unscheduled TODOs.
> +
> +If `org-icalendar-use-scheduled' contains `todo-start' and a task
> +has a \"SCHEDULED\" timestamp, that is always used as the start
> +date.  Otherwise, this variable controls whether a start date is
> +exported and what its value is.

I think it would make sense to link to this variable in the
`org-icalendar-use-scheduled' docstring and possibly in the manual.

-- 
Ihor Radchenko // yantar92,
Org mode contributor,
Learn more about Org mode at <https://orgmode.org/>.
Support Org development at <https://liberapay.com/org-mode>,
or support my work at <https://liberapay.com/yantar92>


^ permalink raw reply	[flat|nested] 21+ messages in thread

* Re: [PATCH] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-06-12 10:36     ` Ihor Radchenko
@ 2023-06-17 17:32       ` Jack Kamm
  2023-06-18 11:28         ` Ihor Radchenko
  0 siblings, 1 reply; 21+ messages in thread
From: Jack Kamm @ 2023-06-17 17:32 UTC (permalink / raw)
  To: Ihor Radchenko; +Cc: emacs-orgmode, mail

[-- Attachment #1: Type: text/plain, Size: 643 bytes --]

Ihor Radchenko <yantar92@posteo.net> writes:

> Would it make sense to throw a warning instead of silently skipping ~++~
> and ~.+~ repeaters?
>
> I think it would make sense to link to this variable in the
> `org-icalendar-use-scheduled' docstring and possibly in the manual.

Thanks, I agree.  I've updated the patch to add a warning for the
nonstandard repeaters (plus a unit test), and also added links to
`org-icalendar-todo-unscheduled-start' in the manual and
`org-icalendar-use-scheduled' docstring.

For convenience I attach these as separate patches here.  If it looks
OK I will squash with the prior patch before applying to main.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0002-ox-icalendar-Display-warning-for-unsupported-repeate.patch --]
[-- Type: text/x-patch, Size: 4906 bytes --]

From 80c05e00335062cc96bdcd85ec507066af4a1d3b Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sat, 17 Jun 2023 07:55:17 -0700
Subject: [PATCH 2/3] ox-icalendar: Display warning for unsupported repeaters

This commit to be squashed with the previous one

* lisp/ox-icalendar.el (org-icalendar--repeater-type): Helper function
to get the repeater type, and display warning if not supported.
* testing/lisp/test-ox-icalendar.el
(test-ox-icalendar/warn-unsupported-repeater): Unit test to warn for
unsupported repeater types.
---
 lisp/ox-icalendar.el              | 30 +++++++++++++++++++++---------
 testing/lisp/test-ox-icalendar.el | 14 ++++++++++++++
 2 files changed, 35 insertions(+), 9 deletions(-)

diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 8c569752b..0dbc623b4 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -810,6 +810,23 @@ (\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are predefined, others
 	    (org-icalendar--valarm entry timestamp summary)
 	    "END:VEVENT")))
 
+(defun org-icalendar--repeater-type (elem)
+  "Return ELEM's repeater-type if supported, else warn and return nil."
+  (let ((repeater-value (org-element-property :repeater-value elem))
+        (repeater-type (org-element-property :repeater-type elem)))
+    (cond
+     ((not (and repeater-type
+                repeater-value
+                (> repeater-value 0)))
+      nil)
+     ;; TODO Add catch-up to supported repeaters (use EXDATE to implement)
+     ((not (memq repeater-type '(cumulate)))
+      (org-display-warning
+       (format "Repeater-type %s not currently supported by iCalendar export"
+               (symbol-name repeater-type)))
+      nil)
+     (repeater-type))))
+
 (defun org-icalendar--vtodo
     (entry uid summary location description categories timezone class)
   "Create a VTODO component.
@@ -826,13 +843,8 @@ (defun org-icalendar--vtodo
 		  (org-element-property :scheduled entry)))
          (dl (and (memq 'todo-due org-icalendar-use-deadline)
                   (org-element-property :deadline entry)))
-         ;; TODO Implement catch-up repeaters using EXDATE
-         (sc-repeat-p (and (eq (org-element-property :repeater-type sc)
-                               'cumulate)
-                           (> (org-element-property :repeater-value sc) 0)))
-         (dl-repeat-p (and (eq (org-element-property :repeater-type dl)
-                               'cumulate)
-                           (> (org-element-property :repeater-value dl) 0)))
+         (sc-repeat-p (org-icalendar--repeater-type sc))
+         (dl-repeat-p (org-icalendar--repeater-type dl))
          (repeat-value (or (org-element-property :repeater-value sc)
                            (org-element-property :repeater-value dl)))
          (repeat-unit (or (org-element-property :repeater-unit sc)
@@ -881,14 +893,14 @@ (defun org-icalendar--vtodo
                              (eq repeat-unit (org-element-property
                                               :repeater-unit dl)))))
               ;; TODO Implement via RDATE with changing DURATION
-              (warn "Not yet implemented: \
+              (org-display-warning "Not yet implemented: \
 different repeaters on SCHEDULED and DEADLINE. Skipping.")
               nil)
              ;; DEADLINE has repeater but SCHEDULED doesn't
              ((and dl-repeat-p (and sc (not sc-repeat-p)))
               ;; TODO SCHEDULED should only apply to first instance;
               ;; use RDATE with custom DURATION to implement that
-              (warn "Not yet implemented: \
+              (org-display-warning "Not yet implemented: \
 repeater on DEADLINE but not SCHEDULED. Skipping.")
               nil)
              ((or sc-repeat-p dl-repeat-p)
diff --git a/testing/lisp/test-ox-icalendar.el b/testing/lisp/test-ox-icalendar.el
index 6a0c961d7..e631b2119 100644
--- a/testing/lisp/test-ox-icalendar.el
+++ b/testing/lisp/test-ox-icalendar.el
@@ -114,5 +114,19 @@ (ert-deftest test-ox-icalendar/todo-repeater-until-utc ()
             (should (re-search-forward "RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=2023050.T..0000Z"))))
       (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
 
+(ert-deftest test-ox-icalendar/warn-unsupported-repeater ()
+  "Test warning is emitted for unsupported repeater type."
+  (let ((org-icalendar-include-todo 'all))
+    (should
+     (member
+      "Repeater-type restart not currently supported by iCalendar export"
+      (org-test-capture-warnings
+       (let ((tmp-ics (org-test-with-temp-text-in-file
+                       "* TODO Unsupported restart repeater
+SCHEDULED: <2023-03-26 Sun .+1m>"
+                       (expand-file-name (org-icalendar-export-to-ics)))))
+         (when (file-exists-p tmp-ics)
+           (delete-file tmp-ics))))))))
+
 (provide 'test-ox-icalendar)
 ;;; test-ox-icalendar.el ends here
-- 
2.40.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0003-ox-icalendar-Links-in-docs-for-org-icalendar-todo-un.patch --]
[-- Type: text/x-patch, Size: 2104 bytes --]

From 94f1c01273878e2a7403c1d47ebabe40595de23d Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sat, 17 Jun 2023 09:59:18 -0700
Subject: [PATCH 3/3] ox-icalendar: Links in docs for
 org-icalendar-todo-unscheduled-start

---
 doc/org-manual.org   | 6 ++++--
 lisp/ox-icalendar.el | 5 +++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index c11694849..89589e32a 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -16054,14 +16054,16 @@ standard iCalendar format.
 #+vindex: org-icalendar-include-todo
 #+vindex: org-icalendar-use-deadline
 #+vindex: org-icalendar-use-scheduled
+#+vindex: org-icalendar-todo-unscheduled-start
 The iCalendar export backend can also incorporate TODO entries based
 on the configuration of the ~org-icalendar-include-todo~ variable.
 The backend exports plain timestamps as =VEVENT=, TODO items as
 =VTODO=, and also create events from deadlines that are in non-TODO
 items.  The backend uses the deadlines and scheduling dates in Org
 TODO items for setting the start and due dates for the iCalendar TODO
-entry.  Consult the ~org-icalendar-use-deadline~ and
-~org-icalendar-use-scheduled~ variables for more details.
+entry.  Consult the ~org-icalendar-use-deadline~,
+~org-icalendar-use-scheduled~, and
+~org-icalendar-todo-unscheduled-start~ variables for more details.
 
 #+vindex: org-icalendar-categories
 #+vindex: org-icalendar-alarm-time
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 0dbc623b4..55ada8e60 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -168,8 +168,9 @@ (defcustom org-icalendar-use-scheduled '(todo-start)
 
 `todo-start'
 
-  Scheduling time stamps in TODO entries become start date.  Some
-  calendar applications show TODO entries only after that date."
+  Scheduling time stamps in TODO entries become start date.  (See
+  also `org-icalendar-todo-unscheduled-start', which controls the
+  start date for TODO entries without a scheduling time stamp)"
   :group 'org-export-icalendar
   :type
   '(set :greedy t
-- 
2.40.1


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* Re: [PATCH] ox-icalendar: Unscheduled tasks & repeating tasks
  2023-06-17 17:32       ` Jack Kamm
@ 2023-06-18 11:28         ` Ihor Radchenko
  0 siblings, 0 replies; 21+ messages in thread
From: Ihor Radchenko @ 2023-06-18 11:28 UTC (permalink / raw)
  To: Jack Kamm; +Cc: emacs-orgmode, mail

Jack Kamm <jackkamm@gmail.com> writes:

> For convenience I attach these as separate patches here.  If it looks
> OK I will squash with the prior patch before applying to main.

Approved.


^ permalink raw reply	[flat|nested] 21+ messages in thread

end of thread, other threads:[~2023-06-18 11:24 UTC | newest]

Thread overview: 21+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2023-03-26 18:56 [RFC] ox-icalendar: Unscheduled tasks & repeating tasks Jack Kamm
2023-03-27 11:59 ` Ihor Radchenko
2023-03-31  5:55   ` Jack Kamm
2023-03-31 13:07     ` Ihor Radchenko
2023-03-31 15:50       ` Jack Kamm
2023-03-31 17:51         ` Ihor Radchenko
2023-03-31 22:20           ` Jack Kamm
2023-04-01  8:30             ` Ihor Radchenko
2023-04-02  0:47               ` Jack Kamm
2023-04-02  8:48                 ` Ihor Radchenko
2023-04-02 15:34                   ` Jack Kamm
2023-04-02 16:32                     ` Ihor Radchenko
2023-04-14 16:57   ` Jack Kamm
2023-04-14 18:46     ` Ihor Radchenko
2023-04-15  3:13       ` Jack Kamm
2023-04-15  9:56         ` Ihor Radchenko
2023-04-16 17:19           ` Jack Kamm
2023-06-11 15:35   ` [PATCH] " Jack Kamm
2023-06-12 10:36     ` Ihor Radchenko
2023-06-17 17:32       ` Jack Kamm
2023-06-18 11:28         ` Ihor Radchenko

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).