From d8c1c48e78bd6cdfbee922c6bda4022c7d7cb9dd Mon Sep 17 00:00:00 2001
From: Lars Henriksen <LarsHenriksen@get2net.dk>
Date: Mon, 25 May 2020 20:52:22 +0200
Subject: Support import of time zones (RFC 5545)

The property parameter time zone identifier (TZID) is recognized in DTSTART,
DTEND and EXDATE and the DATE-TIME value converted to a local time. The time
zone identifier is logged in the note file.

Signed-off-by: Lars Henriksen <LarsHenriksen@get2net.dk>
Signed-off-by: Lukas Fleischer <lfleischer@calcurse.org>
---
 src/calcurse.h |   2 +-
 src/ical.c     | 118 +++++++++++++++++++++++++++++++++++++++++----------------
 src/utils.c    |  23 ++++++-----
 3 files changed, 99 insertions(+), 44 deletions(-)

(limited to 'src')

diff --git a/src/calcurse.h b/src/calcurse.h
index 25c63a0..1f28c37 100644
--- a/src/calcurse.h
+++ b/src/calcurse.h
@@ -1219,7 +1219,7 @@ int get_item_min(time_t);
 struct tm date2tm(struct date, unsigned, unsigned);
 time_t date2sec(struct date, unsigned, unsigned);
 struct date sec2date(time_t);
-time_t utcdate2sec(struct date, unsigned, unsigned);
+time_t tzdate2sec(struct date, unsigned, unsigned, char *);
 int date_cmp(struct date *, struct date *);
 int date_cmp_day(time_t, time_t);
 char *date_sec2date_str(time_t, const char *);
diff --git a/src/ical.c b/src/ical.c
index 5ab6846..d372396 100644
--- a/src/ical.c
+++ b/src/ical.c
@@ -578,6 +578,32 @@ ical_chk_header(FILE * fd, char *buf, char *lstore, unsigned *lineno,
 	return 1;
 }
 
+/*
+ * Return the TZID property parameter value from a DTSTART/DTEND/EXDATE property
+ * in an allocated string. The value may be any text string not containing the
+ * characters '"', ';', ':' and ',' (RFC 5545, sections 3.2.19 and 3.1).
+ */
+static char *ical_get_tzid(char *p)
+{
+	const char param[] = ";TZID=";
+	char *q;
+	int s;
+
+	if (!(p = strstr(p, param)))
+		return NULL;
+	p += sizeof(param) - 1;
+	if (*p == '"')
+		return NULL;
+
+	q = strpbrk(p, ":;");
+	s = q - p + 1;
+	q = mem_malloc(s);
+	strncpy(q, p, s);
+	q[s - 1] = '\0';
+
+	return q;
+}
+
 /*
  * Return event type from a DTSTART/DTEND/EXDATE property.
  */
@@ -601,23 +627,23 @@ static ical_vevent_e ical_get_type(char *c_line)
  * where DATE is 'YYYYMMDD' and TIME is 'HHMMSS'.
  * The time and 'T' separator are optional (in the case of an day-long event).
  *
- * The type argument is either APPOINTMENT or EVENT and the time format must
- * agree.
- *
- * The timezone is not yet handled by calcurse.
+ * The type argument is either APPOINTMENT or EVENT, and the time format must
+ * match (either DATE-TIME or DATE). The time zone identifier is ignored in an
+ * EVENT or in an APPOINTMENT with UTC time.
  */
-static time_t ical_datetime2time_t(char *datestr, ical_vevent_e type)
+static time_t ical_datetime2time_t(char *datestr, char *tzid, ical_vevent_e type)
 {
 	const int INVALID = 0, DATE = 3, DATETIME = 6, DATETIMEZ = 7;
 	struct date date;
 	unsigned hour, min, sec;
-	char c;
+	char c, UTC[] = "";
 	int format;
 
 	EXIT_IF(type == UNDEFINED, "event type not set");
 
 	format = sscanf(datestr, "%04u%02u%02uT%02u%02u%02u%c",
 			&date.yyyy, &date.mm, &date.dd, &hour, &min, &sec, &c);
+
 	if (format == DATE && strlen(datestr) > 8)
 		format = INVALID;
 	if (format == DATETIMEZ && c != 'Z')
@@ -626,9 +652,9 @@ static time_t ical_datetime2time_t(char *datestr, ical_vevent_e type)
 	if (format == DATE && type == EVENT)
 		return date2sec(date, 0, 0);
 	else if (format == DATETIME && type == APPOINTMENT)
-		return date2sec(date, hour, min);
+		return tzdate2sec(date, hour, min, tzid);
 	else if (format == DATETIMEZ && type == APPOINTMENT)
-		return utcdate2sec(date, hour, min);
+		return tzdate2sec(date, hour, min, UTC);
 
 	return 0;
 }
@@ -869,7 +895,7 @@ static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr,
 	 * specified, counts as the first occurrence.
 	 */
 	if ((p = strstr(rrulestr, "UNTIL")) != NULL) {
-		rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, type);
+		rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, NULL, type);
 		if (!(rpt->until)) {
 			ical_log(log, ICAL_VEVENT, itemline,
 				 _("invalid until format."));
@@ -915,47 +941,47 @@ static int
 ical_read_exdate(llist_t * exc, FILE * log, char *exstr, unsigned *noskipped,
 		 const int itemline, ical_vevent_e type)
 {
-	char *p, *q;
+	char *p, *q, *tzid = NULL;
 	time_t t;
 	int n;
 
-	/* See DTSTART. */
 	if (type == UNDEFINED) {
 		ical_log(log, ICAL_VEVENT, itemline,
 			 _("need DTSTART to determine event type."));
-		(*noskipped)++;
-		return 0;
+		goto cleanup;
 	}
 	if (type != ical_get_type(exstr)) {
 		ical_log(log, ICAL_VEVENT, itemline,
 			 _("invalid exception date value type."));
-		(*noskipped)++;
-		return 0;
+		goto cleanup;
 	}
 	p = ical_get_value(exstr);
 	if (!p) {
 		ical_log(log, ICAL_VEVENT, itemline,
 			 _("malformed exceptions line."));
-		(*noskipped)++;
-		return 0;
+		goto cleanup;
 	}
-
+	tzid = ical_get_tzid(exstr);
 	/* Count the exceptions and replace commas by zeroes */
 	for (q = p, n = 1; (q = strchr(q, ',')); *q = '\0', q++, n++)
 		;
 	while (n) {
-		if (!(t = ical_datetime2time_t(p, type))) {
+		if (!(t = ical_datetime2time_t(p, tzid, type))) {
 			ical_log(log, ICAL_VEVENT, itemline,
 				 _("invalid exception."));
-			(*noskipped)++;
-			return 0;
+			goto cleanup;
 		}
 		ical_add_exc(exc, t);
 		p = strchr(p, '\0') + 1;
 		n--;
 	}
-
 	return 1;
+
+cleanup:
+	(*noskipped)++;
+	if (tzid)
+		mem_free(tzid);
+	return 0;
 }
 
 /*
@@ -1050,13 +1076,13 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 	const int ITEMLINE = *lineno - !feof(fdi);
 	ical_vevent_e vevent_type;
 	ical_property_e property;
-	char *p, *note = NULL, *comment;
+	char *p, *note = NULL, *tmp, *tzid;
 	const char *SEPARATOR = "-- \n";
 	struct string s;
 	struct {
 		llist_t exc;
 		ical_rpt_t *rpt;
-		char *mesg, *desc, *loc, *comm, *stat, *note;
+		char *mesg, *desc, *loc, *comm, *stat, *imp, *note;
 		long start, end, dur;
 		int has_alarm;
 	} vevent;
@@ -1129,6 +1155,11 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 						    vevent.stat);
 					mem_free(vevent.stat);
 				}
+				if (vevent.imp) {
+					string_catf(&s, ("Import: %s\n"),
+						    vevent.imp);
+					mem_free(vevent.imp);
+				}
 				vevent.note = generate_note(string_buf(&s));
 				mem_free(s.buf);
 			}
@@ -1167,6 +1198,18 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 			 * event.
 			 */
 			vevent_type = ical_get_type(buf);
+			if ((tzid = ical_get_tzid(buf)) &&
+			    vevent_type == APPOINTMENT) {
+				/* Add note on TZID. */
+				if (vevent.imp) {
+					asprintf(&tmp, "%s, TZID=%s",
+						 vevent.imp, tzid);
+					mem_free(vevent.imp);
+					vevent.imp = tmp;
+				} else
+					asprintf(&vevent.imp, "TZID=%s", tzid);
+				has_note = separator = 1;
+			}
 			p = ical_get_value(buf);
 			if (!p) {
 				ical_log(log, ICAL_VEVENT, ITEMLINE,
@@ -1174,7 +1217,9 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 				goto skip;
 			}
 
-			vevent.start = ical_datetime2time_t(p, vevent_type);
+			vevent.start = ical_datetime2time_t(p, tzid, vevent_type);
+			if (tzid)
+				mem_free(tzid);
 			if (!vevent.start) {
 				ical_log(log, ICAL_VEVENT, ITEMLINE,
 					 _("invalid or malformed event "
@@ -1194,6 +1239,7 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 					 _("invalid end time value type."));
 				goto skip;
 			}
+			tzid = ical_get_tzid(buf);
 			p = ical_get_value(buf);
 			if (!p) {
 				ical_log(log, ICAL_VEVENT, ITEMLINE,
@@ -1201,7 +1247,9 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 				goto skip;
 			}
 
-			vevent.end = ical_datetime2time_t(p, vevent_type);
+			vevent.end = ical_datetime2time_t(p, tzid, vevent_type);
+			if (tzid)
+				mem_free(tzid);
 			if (!vevent.end) {
 				ical_log(log, ICAL_VEVENT, ITEMLINE,
 					 _("malformed event end time."));
@@ -1257,7 +1305,8 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 					      ICAL_VEVENT, ITEMLINE, log);
 			if (!note)
 				goto cleanup;
-			separator = (property != DESCRIPTION);
+			if (!separator)
+				separator = (property != DESCRIPTION);
 			has_note = 1;
 		}
 		switch (property) {
@@ -1280,10 +1329,10 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
 		case COMMENT:
 			/* There may be more than one. */
 			if (vevent.comm) {
-				asprintf(&comment, "%sComment: %s",
+				asprintf(&tmp, "%sComment: %s",
 					 vevent.comm, note);
 				mem_free(vevent.comm);
-				vevent.comm = comment;
+				vevent.comm = tmp;
 			} else
 				vevent.comm = note;
 			break;
@@ -1322,6 +1371,8 @@ cleanup:
 		mem_free(vevent.comm);
 	if (vevent.stat)
 		mem_free(vevent.stat);
+	if (vevent.imp)
+		mem_free(vevent.imp);
 	if (vevent.mesg)
 		mem_free(vevent.mesg);
 	if (vevent.rpt)
@@ -1335,7 +1386,7 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
 {
 	const int ITEMLINE = *lineno - !feof(fdi);
 	ical_property_e property;
-	char *note = NULL, *comment;
+	char *note = NULL, *tmp;
 	const char *SEPARATOR = "-- \n";
 	struct string s;
 	struct {
@@ -1430,7 +1481,8 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
 					       ICAL_VTODO, ITEMLINE, log);
 			if (!note)
 				goto cleanup;
-			separator = (property != DESCRIPTION);
+			if (!separator)
+				separator = (property != DESCRIPTION);
 			has_note = 1;
 		}
 		switch (property) {
@@ -1453,10 +1505,10 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
 		case COMMENT:
 			/* There may be more than one. */
 			if (vtodo.comm) {
-				asprintf(&comment, "%sComment: %s",
+				asprintf(&tmp, "%sComment: %s",
 					 vtodo.comm, note);
 				mem_free(vtodo.comm);
-				vtodo.comm = comment;
+				vtodo.comm = tmp;
 			} else
 				vtodo.comm = note;
 			break;
diff --git a/src/utils.c b/src/utils.c
index 6f849ea..23b9d89 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -423,22 +423,25 @@ struct date sec2date(time_t t)
 	return d;
 }
 
-time_t utcdate2sec(struct date day, unsigned hour, unsigned min)
+time_t tzdate2sec(struct date day, unsigned hour, unsigned min, char *tznew)
 {
-	char *tz;
+	char *tzold;
 	time_t t;
 
-	tz = getenv("TZ");
-	if (tz)
-		tz = mem_strdup(tz);
-	setenv("TZ", "", 1);
-	tzset();
+	if (!tznew)
+		return date2sec(day, hour, min);
+
+	tzold = getenv("TZ");
+	if (tzold)
+		tzold = mem_strdup(tzold);
 
+	setenv("TZ", tznew, 1);
+	tzset();
 	t = date2sec(day, hour, min);
 
-	if (tz) {
-		setenv("TZ", tz, 1);
-		mem_free(tz);
+	if (tzold) {
+		setenv("TZ", tzold, 1);
+		mem_free(tzold);
 	} else {
 	    unsetenv("TZ");
 	}
-- 
cgit v1.2.3-70-g09d2