9front - general discussion about 9front
 help / color / mirror / Atom feed
* smtp/imap date bug, messages from the future
@ 2020-07-31  7:59 sirjofri+ml-9front
  2020-07-31  8:02 ` [9front] " sirjofri+ml-9front
  2020-07-31 15:27 ` ori
  0 siblings, 2 replies; 4+ messages in thread
From: sirjofri+ml-9front @ 2020-07-31  7:59 UTC (permalink / raw)
  To: 9front

Hello all,

Some time ago I noticed some bug that's not critical. I want to share this with you.

I live in Europe, timezone CES. That's important because it's involved in this bug, I think. The time difference is exactly my timezone shift, as far as I investigated.

All mails I open via imap have this bug, but it's not the mails itself, it's the imap. It seems like mails are saved (via smtp) without the timezone applied¹, but when I open them imap (or upas?) seems to apply the timezone again, resulting in a future date.

I should note that this bug appears not only with a local upas acme mail reader, but also with other imap clients on different platforms. That's why I think it's the imapd server, not the clients, and also not the saved mail messages (smtpd).

To mention an example: I sent a mail from my smtp client to my server at 09:26 CES. The message is saved as 1596180390.00, which results in that exact time. My .idx file also has that exact timestamp. My IMAP clients (upasfs/acme as well as non-plan9) report a time from the future (11:26).

Looking into upasfs directory for this mail and catting some files:
date:      07:27 UTC (correct, add two hours because of timezone)
unixdate:  11:26 CES (two hours in the future)

My non-plan9 IMAP client reports:
Sent date: 09:49 CES (correct; equivalent to date file)
Recv date: 11:49 CES (two hours in the future, equivalent to unixdate file)

I'll try investigating further and send patches if I can manage to fix this.

sirjofri

———
¹ date mailstamp is correct with local time; stamp from /mail/box.



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

* Re: [9front] smtp/imap date bug, messages from the future
  2020-07-31  7:59 smtp/imap date bug, messages from the future sirjofri+ml-9front
@ 2020-07-31  8:02 ` sirjofri+ml-9front
  2020-07-31 15:27 ` ori
  1 sibling, 0 replies; 4+ messages in thread
From: sirjofri+ml-9front @ 2020-07-31  8:02 UTC (permalink / raw)
  To: 9front

Hello again,

Checking the receiving side (in this case gmail): The time is correct. I just don't know if that's the time the server received the mail or the server thinks the mail is sent.

sirjofri



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

* Re: [9front] smtp/imap date bug, messages from the future
  2020-07-31  7:59 smtp/imap date bug, messages from the future sirjofri+ml-9front
  2020-07-31  8:02 ` [9front] " sirjofri+ml-9front
@ 2020-07-31 15:27 ` ori
  2020-08-08 11:25   ` sirjofri+ml-9front
  1 sibling, 1 reply; 4+ messages in thread
From: ori @ 2020-07-31 15:27 UTC (permalink / raw)
  To: 9front

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

> Hello all,
> 
> Some time ago I noticed some bug that's not critical. I want to share this with you.
> 
> I live in Europe, timezone CES. That's important because it's involved in this bug, I think. The time difference is exactly my timezone shift, as far as I investigated.
> 
> All mails I open via imap have this bug, but it's not the mails itself, it's the imap. It seems like mails are saved (via smtp) without the timezone applied¹, but when I open them imap (or upas?) seems to apply the timezone again, resulting in a future date.
> 
> I should note that this bug appears not only with a local upas acme mail reader, but also with other imap clients on different platforms. That's why I think it's the imapd server, not the clients, and also not the saved mail messages (smtpd).
> 
> To mention an example: I sent a mail from my smtp client to my server at 09:26 CES. The message is saved as 1596180390.00, which results in that exact time. My .idx file also has that exact timestamp. My IMAP clients (upasfs/acme as well as non-plan9) report a time from the future (11:26).
> 
> Looking into upasfs directory for this mail and catting some files:
> date:      07:27 UTC (correct, add two hours because of timezone)
> unixdate:  11:26 CES (two hours in the future)
> 
> My non-plan9 IMAP client reports:
> Sent date: 09:49 CES (correct; equivalent to date file)
> Recv date: 11:49 CES (two hours in the future, equivalent to unixdate file)
> 
> I'll try investigating further and send patches if I can manage to fix this.
> 
> sirjofri
> 
> ———
> ¹ date mailstamp is correct with local time; stamp from /mail/box.

First off -- to confirm, you're running with upas/imap4d,
and upas/fs, as well as whatever other clients you use (eg,
your phone) are reporting the wrong time?

Can you try applying the rewrite of our date handling code
and see if maybe I've accidentally fixed the issue? I've
attached the most current version of that chaange so you
dont' need to go hunt around for it.

If that doesn't work, can you echo 'debug' into /mail/fs/ctl,
and give some of the dates that are coming out of the imap
server?

[-- Attachment #2: Type: text/plain, Size: 55312 bytes --]

diff -r c5110aa667d8 sys/include/libc.h
--- a/sys/include/libc.h	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/include/libc.h	Fri Jul 31 08:22:43 2020 -0700
@@ -314,22 +314,44 @@
 /*
  * Time-of-day
  */
+typedef struct Tzone Tzone;
+#pragma incomplete Tzone
+
 
 typedef
 struct Tm
 {
-	int	sec;
-	int	min;
-	int	hour;
-	int	mday;
-	int	mon;
-	int	year;
-	int	wday;
-	int	yday;
-	char	zone[4];
-	int	tzoff;
+	int	nsec;		/* nseconds (range 0...1e9) */
+	int	sec;		/* seconds (range 0..60) */
+	int	min;		/* minutes (0..59) */
+	int	hour;		/* hours (0..23) */
+	int	mday;		/* day of the month (1..31) */
+	int	mon;		/* month of the year (0..11) */
+	int	year;		/* year A.D. */
+	int	wday;		/* day of week (0..6, Sunday = 0) */
+	int	yday;		/* day of year (0..365) */
+	char	zone[16];	/* time zone name */
+	int	tzoff;		/* time zone delta from GMT */
+	Tzone	*tz;		/* time zone associated with this date */
 } Tm;
 
+typedef
+struct Tmfmt {
+	char	*fmt;
+	Tm	*tm;
+} Tmfmt;
+
+#pragma varargck	type	"τ"	Tmfmt
+
+extern	Tzone*	tzload(char *name);
+extern	Tm*	tmnow(Tm*, Tzone*);
+extern	Tm*	tmtime(Tm*, vlong, Tzone*);
+extern	Tm*	tmtimens(Tm*, vlong, int, Tzone*);
+extern	Tm*	tmparse(Tm*, char*, char*, Tzone*, char **ep);
+extern	vlong	tmnorm(Tm*);
+extern	Tmfmt	tmfmt(Tm*, char*);
+extern	void	tmfmtinstall(void);
+
 extern	Tm*	gmtime(long);
 extern	Tm*	localtime(long);
 extern	char*	asctime(Tm*);
diff -r c5110aa667d8 sys/man/2/tmdate
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sys/man/2/tmdate	Fri Jul 31 08:22:43 2020 -0700
@@ -0,0 +1,275 @@
+.TH TMDATE 2
+.SH NAME
+tmnow, tzload, tmtime, tmparse, tmfmt, tmnorm, - convert date and time
+.SH SYNOPSIS
+.B #include <u.h>
+.br
+.B #include <libc.h>
+.PP
+.ft L
+.nf
+.EX
+typedef struct Tmd Tmd;
+typedef struct Tmfmt Tmfmt;
+
+struct {
+	int	nsec;	/* nanoseconds (range 0..1e9) */
+	int	sec;	/* seconds (range 0..59) */
+	int	min;	/* minutes (0..59) */
+	int	hour;	/* hours (0..23) */
+	int	mday;	/* day of the month (1..31) */
+	int	mon;	/* month of the year (0..11) */
+	int	year;	/* C.E year - 1900 */
+	int	wday;	/* day of week (0..6, Sunday = 0) */
+	int	yday;	/* day of year (0..365) */
+	char	zone[];	/* time zone name */
+	int	tzoff;	/* time	zone delta from GMT, seconds */
+};
+
+Tzone *tzload(char *name);
+Tm    *tmnow(Tm *tm, char *tz);
+Tm    *tmtime(Tm *tm, vlong abs, Tzone *tz);
+Tm    *tmtimens(Tm *tm, vlong abs, int ns, Tzone *tz);
+Tm    *tmparse(Tm *dst, char *fmt, char *tm, Tzone *zone, char **ep);
+vlong  tmnorm(Tm *tm);
+Tmfmt  tmfmt(Tm *tm, char *fmt);
+void   tmfmtinstall(void);
+.EE
+.SH DESCRIPTION
+.PP
+This family of functions handles simple date and time manipulation.
+.PP
+Time zones are loaded by as name.
+They can be specified as the abbreviated timezone name,
+the full timezone name, the path to a timezone file,
+or an absolute offset in the HHMM form.
+.PP
+When given as a timezone, any instant-dependent adjustments such as leap
+seconds and daylight savings time will be applied to the derived fields of
+struct tm, but will not affect the absolute time.
+The time zone name local always refers to the time in /env/timezone.
+The nil timezone always refers to GMT.
+.PP
+Tzload loads a timezone by name. The returned timezone is
+cached for the lifetime of the program, and should not be freed.
+Loading a timezone repeatedly by name loads from the cache, and
+does not leak.
+.PP
+Tmnow gets the current time of day in the requested time zone.
+.PP
+Tmtime converts the millisecond-resolution timestamp 'abs'
+into a Tm struct in the requested timezone.
+Tmtimens does the same, but with a nanosecond accuracy.
+.PP
+Tmstime is identical to tmtime, but accepts the time in sec-
+onds.
+.PP
+Tmparse parses a time from a string according to the format argument.
+The point at which the parsing stopped is returned in
+.IR ep .
+If
+.I ep
+is nil, trailing garbage is ignored.
+The result is returned in the timezone requested.
+If there is a timezone in the date, and a timezone is provided
+when parsing, then the zone is shifted to the provided timezone.
+Parsing is case-insensitive
+.PP
+The format argument contains zero or more of the following components:
+.TP
+.B Y, YY, YYYY
+Represents the year.
+.I YY
+prints the year in 2 digit form.
+.TP
+.B M, MM, MMM, MMMM
+The month of the year, in unpadded numeric, padded numeric, short name, or long name,
+respectively.
+.TP
+.B D, DD
+The day of month in unpadded or padded numeric form, respectively.
+.TP
+.B W, WW, WWW
+The day of week in numeric, short or long name form, respectively.
+.TP
+.B h, hh
+The hour in unpadded or padded form, respectively
+.TP
+.B m, mm
+The minute in unpadded or padded form, respectively
+.TP
+.B s, ss
+The second in unpadded or padded form, respectively
+.TP
+.B t, tt
+The milliseconds in unpadded and padded form, respectively.
+.B u, uu, uuu, uuuu
+The microseconds in unpadded. padded form modulo milliseconds,
+or unpadded, padded forms of the complete value, respectively.
+.B n, nn, nnn, nnnn, nnnnn, nnnnnn
+The nanoseconds in unpadded and padded form modulo milliseconds,
+the unpadded and padded form modulo microseconds,
+and the unpadded and padded complete value, respectively.
+.TP
+.B Z, ZZ, ZZZ
+The timezone in [+-]HHMM and [+-]HH:MM, and named form, respectively.
+.TP
+.B a, A
+Lower and uppercase 'am' and 'pm' specifiers, respectively.
+.TP
+.B [...]
+Quoted text, copied directly to the output.
+.TP
+.B _
+When formatting, this inserts padding into the date format.
+The padded width of a field is the sum of format and specifier
+characters combined. When
+For example,
+.I __h
+will format to a width of 3. When parsing, this acts as whitespace.
+.TP
+.B ?
+When parsing, this makes the following argument match fuzzily.
+Fuzzy matching means that all formats are tried, from most to least specific.
+For example, 
+.I ?M
+will match 
+.IR January ,
+.IR Jan ,
+.IR 01 ,
+and 
+.IR 1 ,
+in that order of preference.
+.TP
+.B ~
+When parsing a date, this slackens range enforcement, accepting
+out of range values such as January
+.IR 32 ,
+which would get normalized to February 1st.
+.PP
+Any characters not specified above are copied directly to output,
+without modification.
+
+
+
+.PP
+If the format argument is nil, it makes an
+attempt to parse common human readable date formats.  These
+formats include ISO-8601,RFC-3339 and RFC-2822 dates.
+.
+.PP
+Tmfmt produces a format description structure suitable for passing
+to
+.IR fmtprint (2) .
+If  fmt is nil, we default to the format used in
+.IR ctime (2).
+The format of the format string is identical to
+.IR tmparse.
+
+.PP
+When parsing, any amount of whitespace is treated as a single token.
+All string matches are case insensitive, and zero padding is optional.
+
+.PP
+Tmnorm takes a manually adjusted Tm structure, and normalizes it,
+returning the absolute timestamp that the date represents.
+Normalizing recomputes the
+.I year, mon, mday, hr, min, sec
+and
+.I tzoff
+fields.
+If
+.I tz
+is non-nil, then
+.I tzoff
+will be recomputed, taking into account daylight savings
+for the absolute time.
+The values not used in the computation are recomputed for
+the resulting absolute time.
+All out of range values are wrapped.
+For example, December 32 will roll over to Jan 1 of the
+following year.
+.PP
+Tmfmtinstall installs a time format specifier %τ. The time
+format behaves as in tmfmt
+
+.SH EXAMPLES
+.PP
+All examples assume tmfmtinstall has been called.
+.PP
+Get the current date in the local timezone, UTC, and
+US_Pacific time. Print it using the default format.
+
+.IP
+.EX
+Tm t;
+Tzone *zl, *zp;
+if((zl = tzload("local") == nil)
+	sysfatal("load zone: %r");
+if((zp = tzload("US_Pacific") == nil)
+	sysfatal("load zone: %r");
+print("local: %τ\\n", tmfmt(tmnow(&t, zl), nil));
+print("gmt: %τ\\n", tmfmt(tmnow(&t, nil), nil));
+print("eastern: %τ\\n", tmfmt(tmnow(&t, zp), nil));
+.EE
+.PP
+Compare if two times are the same, regardless of timezone.
+Done with full, strict error checking.
+
+.IP
+.EX
+Tm a, b;
+
+if(tmparse(&a, nil, "Tue Dec 10 12:36:00 PST 2019", &e) == nil)
+	sysfatal("failed to parse: %r");
+if(*e != '\0')
+	sysfatal("trailing junk %s", e);
+if(tmparse(&b, nil, "Tue Dec 10 15:36:00 EST 2019", &e) == nil)
+	sysfata("failed to parse: %r");
+if(*e != '\0')
+	sysfatal("trailing junk %s", e);
+if(tmnorm(a) == tmnorm(b) && a.nsec == b.nsec)
+	print("same\\n");
+else
+	print("different\\n");
+.EE
+
+.PP
+Convert from one timezone to another.
+
+.IP
+.EX
+Tm here, there;
+Tzone *zl, *zp;
+if((zl = tzload("local")) == nil)
+	sysfatal("load zone: %r");
+if((zp = tzload("US_Pacific")) == nil)
+	sysfatal("load zone: %r");
+if(tmnow(&here, zl) == nil)
+	sysfatal("get time: %r");
+if(tmtime(&there, tmnorm(&tm), zp) == nil)
+	sysfatal("shift time: %r");
+.EE
+
+.PP
+Add a day. Because cross daylight savings, only 23 hours are added.
+
+.EX
+Tm t;
+char *date = "Sun Nov 2 13:11:11 PST 2019";
+if(tmparse(&t, "W MMM D hh:mm:ss z YYYY, date, &e) == nil)
+	print("failed top parse");
+tm.day++;
+tmnorm(&t);
+print("%τ", &t); /*  Mon Nov 3 13:11:11 PST 2019 */
+.EE
+
+.SH BUGS
+.PP
+There is no way to format specifier for subsecond precision.
+.PP
+The timezone information that we ship is out of date.
+.PP
+The plan 9 timezone format has no way to express leap seconds.
+.PP
+We provide no way to manipulate timezones.
diff -r c5110aa667d8 sys/src/cmd/upas/common/common.h
--- a/sys/src/cmd/upas/common/common.h	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/common/common.h	Fri Jul 31 08:22:43 2020 -0700
@@ -52,9 +52,6 @@
 void	mailfmtinstall(void);	/* 'U' = 2047fmt */
 #pragma varargck	type	"U"	char*
 
-/* totm.c */
-int	fromtotm(char*, Tm*);
-
 /* a pipe between parent and child*/
 typedef struct{
 	Biobuf	bb;
diff -r c5110aa667d8 sys/src/cmd/upas/common/folder.c
--- a/sys/src/cmd/upas/common/folder.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/common/folder.c	Fri Jul 31 08:22:43 2020 -0700
@@ -1,5 +1,7 @@
 #include "common.h"
 
+#define Ctimefmt "WW MMM _D hh:mm:ss ZZZ YYYY"
+
 enum{
 	Mbox	= 1,
 	Mdir,
@@ -185,7 +187,7 @@
 appendfolder(Biobuf *b, char *addr, int fd)
 {
 	char *s;
-	int r;
+	int r, n;
 	Biobuf bin;
 	Folder *f;
 	Tm tm;
@@ -194,9 +196,10 @@
 	Bseek(f->out, 0, 2);
 	Binit(&bin, fd, OREAD);
 	s = Brdstr(&bin, '\n', 0);
-	if(!s || strncmp(s, "From ", 5))
+	n = strlen(s);
+	if(!s || strncmp(s, "From ", 5) != 0)
 		Bprint(f->out, "From %s %.28s\n", addr, ctime(f->t));
-	else if(fromtotm(s, &tm) >= 0)
+	else if(n > 5 && tmparse(&tm, Ctimefmt, s + 5, nil, nil) != nil)
 		f->t = tm2sec(&tm);
 	if(s)
 		Bwrite(f->out, s, strlen(s));
diff -r c5110aa667d8 sys/src/cmd/upas/common/mkfile
--- a/sys/src/cmd/upas/common/mkfile	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/common/mkfile	Fri Jul 31 08:22:43 2020 -0700
@@ -10,7 +10,6 @@
 	fmt.$O\
 	libsys.$O\
 	process.$O\
-	totm.$O\
 
 HFILES=common.h\
 	sys.h\
diff -r c5110aa667d8 sys/src/cmd/upas/common/totm.c
--- a/sys/src/cmd/upas/common/totm.c	Wed Jul 29 13:56:03 2020 +0930
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-#include <common.h>
-
-static char mtab[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
-
-int
-ctimetotm(char *s, Tm *tm)
-{
-	char buf[32];
-
-	if(strlen(s) < 28)
-		return -1;
-	snprint(buf, sizeof buf, "%s", s);
-	memset(tm, 0, sizeof *tm);
-	buf[7] = 0;
-	tm->mon = (strstr(mtab, buf+4) - mtab)/3;
-	tm->mday = atoi(buf+8);
-	tm->hour = atoi(buf+11);
-	tm->min = atoi(buf+14);
-	tm->sec = atoi(buf+17);
-	tm->zone[0] = buf[20];
-	tm->zone[1] = buf[21];
-	tm->zone[2] = buf[22];
-	tm->year = atoi(buf+24) - 1900;
-	return 0;
-}
-
-int
-fromtotm(char *s, Tm *tm)
-{
-	char buf[256], *f[3];
-
-	snprint(buf, sizeof buf, "%s", s);
-	if(getfields(buf, f, nelem(f), 0, " ") != 3)
-		return -1;
-	return ctimetotm(f[2], tm);
-}
diff -r c5110aa667d8 sys/src/cmd/upas/fs/imap.c
--- a/sys/src/cmd/upas/fs/imap.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/fs/imap.c	Fri Jul 31 08:22:43 2020 -0700
@@ -268,13 +268,10 @@
 internaltounix(char *s)
 {
 	Tm tm;
-	if(strlen(s) < 20 || s[2] != '-' || s[6] != '-')
+
+	if(tmparse(&tm, "?DD-?MM-YYYY hh:mm:ss ?Z", s, nil, nil) == nil)
 		return -1;
-	s[2] = ' ';
-	s[6] = ' ';
-	if(strtotm(s, &tm) == -1)
-		return -1;
-	return tm2sec(&tm);
+	return tmnorm(&tm);
 }
 	
 static char*
@@ -981,7 +978,7 @@
 		}
 		if(c < 0){
 			/* new message */
-			idprint(imap, "new: %U (%U)\n", f[i].uid, m? m->imapuid: 0);
+			idprint(imap, "new: %U (%U)\n", f[i].uid, m ? m->imapuid: 0);
 			if(f[i].sizes == 0 || f[i].sizes > Maxmsg){
 				idprint(imap, "skipping bad size: %lud\n", f[i].sizes);
 				i++;
diff -r c5110aa667d8 sys/src/cmd/upas/fs/strtotm.c
--- a/sys/src/cmd/upas/fs/strtotm.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/fs/strtotm.c	Fri Jul 31 08:22:43 2020 -0700
@@ -1,98 +1,27 @@
 #include <u.h>
 #include <libc.h>
 
-static char*
-skiptext(char *q)
+int
+strtotm(char *s, Tm *t)
 {
-	while(*q != '\0' && *q != ' ' && *q != '\t' && *q != '\r' && *q != '\n')
-		q++;
-	return q;
+	char **f, *fmt[] = {
+		"WW MMM DD hh:mm:ss ?Z YYYY",
+		"?WW ?DD ?MMM ?YYYY hh:mm:ss ?Z",
+		"?WW ?DD ?MMM ?YYYY hh:mm:ss",
+		"?WW, DD-?MM-YY",
+		"?DD ?MMM ?YYYY hh:mm:ss ?Z",
+		"?DD ?MMM ?YYYY hh:mm:ss",
+		"?DD-?MM-YY hh:mm:ss ?Z",
+		"?DD-?MM-YY hh:mm:ss",
+		"?DD-?MM-YY",
+		"?MMM/?DD/?YYYY hh:mm:ss ?Z",
+		"?MMM/?DD/?YYYY hh:mm:ss",
+		"?MMM/?DD/?YYYY",
+		nil,
+	};
+
+	for(f = fmt; *f; f++)
+		if(tmparse(t, *f, s, nil, nil) != nil)
+			return 0;
+	return -1;
 }
-
-static char*
-skipwhite(char *q)
-{
-	while(*q == ' ' || *q == '\t' || *q == '\r' || *q == '\n')
-		q++;
-	return q;
-}
-
-static char* months[] = {
-	"jan", "feb", "mar", "apr",
-	"may", "jun", "jul", "aug",
-	"sep", "oct", "nov", "dec"
-};
-
-int
-strtotm(char *p, Tm *t)
-{
-	char *q, *r;
-	int j;
-	Tm tm;
-	int delta;
-
-	delta = 0;
-	memset(&tm, 0, sizeof(tm));
-	tm.mon = -1;
-	tm.hour = -1;
-	tm.min = -1;
-	tm.year = -1;
-	tm.mday = -1;
-	memcpy(tm.zone, "GMT", 3);
-	for(p = skipwhite(p); *p; p = skipwhite(q)){
-		q = skiptext(p);
-
-		/* look for time in hh:mm[:ss] */
-		if(r = memchr(p, ':', q - p)){
-			tm.hour = strtol(p, 0, 10);
-			tm.min = strtol(r + 1, 0, 10);
-			if(r = memchr(r + 1, ':', q - (r + 1)))
-				tm.sec = strtol(r + 1, 0, 10);
-			else
-				tm.sec = 0;
-			continue;
-		}
-
-		/* look for month */
-		for(j = 0; j < 12; j++)
-			if(cistrncmp(p, months[j], 3) == 0){
-				tm.mon = j;
-				break;
-			}
-		if(j != 12)
-			continue;
-
-		/* look for time zone [A-Z][A-Z]T */
-		if(q - p == 3)
-		if(p[0] >= 'A' && p[0] <= 'Z')
-		if(p[1] >= 'A' && p[1] <= 'Z')
-		if(p[2] == 'T'){
-			strecpy(tm.zone, tm.zone + 4, p);
-			continue;
-		}
-
-		if(p[0] == '+'||p[0] == '-')
-		if(q - p == 5 && strspn(p + 1, "0123456789") == 4){
-			delta = (((p[1] - '0')*10 + p[2] - '0')*60 + (p[3] - '0')*10 + p[4] - '0')*60;
-			if(p[0] == '-')
-				delta = -delta;
-			continue;
-		}
-		if(strspn(p, "0123456789") == q - p){
-			j = strtol(p, nil, 10);
-			if(j >= 1 && j <= 31)
-				tm.mday = j;
-			if(j >= 1900)
-				tm.year = j - 1900;
-			continue;
-		}
-		//eprint("strtotm: garbage %.*s\n", utfnlen(p, q - p), p);
-	}
-	if(tm.mon < 0 || tm.year < 0
-	|| tm.hour < 0 || tm.min < 0
-	|| tm.mday < 0)
-		return -1;
-
-	*t = *localtime(tm2sec(&tm) - delta);
-	return 0;
-}
diff -r c5110aa667d8 sys/src/cmd/upas/imap4d/date.c
--- a/sys/src/cmd/upas/imap4d/date.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/imap4d/date.c	Fri Jul 31 08:22:43 2020 -0700
@@ -1,142 +1,10 @@
 #include "imap4d.h"
 
-static char *wdayname[] = {
-	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
-};
-
-static char *monname[] = {
-	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
-};
-
-/*
- * zone	: [A-Za-z][A-Za-z][A-Za-z]	some time zone names
- *	| [A-IK-Z]			military time; rfc1123 says the rfc822 spec is wrong.
- *	| "UT"				universal time
- *	| [+-][0-9][0-9][0-9][0-9]
- * zones is the rfc-822 list of time zone names
- */
-static Namedint zones[] =
-{
-	{"A",	-1 * 3600},
-	{"B",	-2 * 3600},
-	{"C",	-3 * 3600},
-	{"CDT", -5 * 3600},
-	{"CST", -6 * 3600},
-	{"D",	-4 * 3600},
-	{"E",	-5 * 3600},
-	{"EDT", -4 * 3600},
-	{"EST", -5 * 3600},
-	{"F",	-6 * 3600},
-	{"G",	-7 * 3600},
-	{"GMT", 0},
-	{"H",	-8 * 3600},
-	{"I",	-9 * 3600},
-	{"K",	-10 * 3600},
-	{"L",	-11 * 3600},
-	{"M",	-12 * 3600},
-	{"MDT", -6 * 3600},
-	{"MST", -7 * 3600},
-	{"N",	+1 * 3600},
-	{"O",	+2 * 3600},
-	{"P",	+3 * 3600},
-	{"PDT", -7 * 3600},
-	{"PST", -8 * 3600},
-	{"Q",	+4 * 3600},
-	{"R",	+5 * 3600},
-	{"S",	+6 * 3600},
-	{"T",	+7 * 3600},
-	{"U",	+8 * 3600},
-	{"UT",	0},
-	{"V",	+9 * 3600},
-	{"W",	+10 * 3600},
-	{"X",	+11 * 3600},
-	{"Y",	+12 * 3600},
-	{"Z",	0},
-};
-
-static void
-zone2tm(Tm *tm, char *s)
-{
-	int i;
-	Tm aux, *atm;
-
-	if(*s == '+' || *s == '-'){
-		i = strtol(s, &s, 10);
-		tm->tzoff = (i/100)*3600 + i%100;
-		strncpy(tm->zone, "", 4);
-		return;
-	}
-
-	/*
-	 * look it up in the standard rfc822 table
-	 */
-	strncpy(tm->zone, s, 3);
-	tm->zone[3] = 0;
-	tm->tzoff = 0;
-	for(i = 0; i < nelem(zones); i++){
-		if(cistrcmp(zones[i].name, s) == 0){
-			tm->tzoff = zones[i].v;
-			return;
-		}
-	}
-
-	/*
-	 * one last try: look it up in the current local timezone
-	 * probe a couple of times to get daylight/standard time change.
-	 */
-	aux = *tm;
-	memset(aux.zone, 0, 4);
-	aux.hour--;
-	for(i = 0; i < 2; i++){
-		atm = localtime(tm2sec(&aux));
-		if(cistrcmp(tm->zone, atm->zone) == 0){
-			tm->tzoff = atm->tzoff;
-			return;
-		}
-		aux.hour++;
-	}
-
-	strncpy(tm->zone, "GMT", 4);
-	tm->tzoff = 0;
-}
-
-/*
- * hh[:mm[:ss]]
- */
-static void
-time2tm(Tm *tm, char *s)
-{
-	tm->hour = strtoul(s, &s, 10);
-	if(*s++ != ':')
-		return;
-	tm->min = strtoul(s, &s, 10);
-	if(*s++ != ':')
-		return;
-	tm->sec = strtoul(s, &s, 10);
-}
-
-static int
-dateindex(char *d, char **tab, int n)
-{
-	int i;
-
-	for(i = 0; i < n; i++)
-		if(cistrcmp(d, tab[i]) == 0)
-			return i;
-	return -1;
-}
-
 int
 imap4date(Tm *tm, char *date)
 {
-	char *flds[4];
-
-	if(getfields(date, flds, 3, 0, "-") != 3)
+	if(tmparse(tm, "DD-?MM-YYYY hh:mm:ss ?Z", date, nil, nil) == nil)
 		return 0;
-
-	tm->mday = strtol(flds[0], nil, 10);
-	tm->mon = dateindex(flds[1], monname, 12);
-	tm->year = strtol(flds[2], nil, 10) - 1900;
 	return 1;
 }
 
@@ -146,29 +14,17 @@
 ulong
 imap4datetime(char *date)
 {
-	char *flds[4], *sflds[4];
-	ulong t;
 	Tm tm;
+	vlong s;
 
-	if(getfields(date, flds, 4, 0, " ") != 3)
-		return ~0;
-
-	if(!imap4date(&tm, flds[0]))
-		return ~0;
-
-	if(getfields(flds[1], sflds, 3, 0, ":") != 3)
-		return ~0;
-
-	tm.hour = strtol(sflds[0], nil, 10);
-	tm.min = strtol(sflds[1], nil, 10);
-	tm.sec = strtol(sflds[2], nil, 10);
-
-	strcpy(tm.zone, "GMT");
-	tm.yday = 0;
-	t = tm2sec(&tm);
-	zone2tm(&tm, flds[2]);
-	t -= tm.tzoff;
-	return t;
+	s = -1;
+	if(tmparse(&tm, "?DD-?MM-YYYY hh:mm:ss ?Z", date, nil, nil) != nil)
+		s = tmnorm(&tm);
+	else if(tmparse(&tm, "?W, ?DD-?MM-YYYY hh:mm:ss ?Z", date, nil, nil) != nil)
+		s = tmnorm(&tm);
+	if(s > 0 && s < (1ULL<<31))
+		return s;
+	return ~0;
 }
 
 /*
@@ -181,85 +37,18 @@
 Tm*
 date2tm(Tm *tm, char *date)
 {
-	char *flds[7], *s, dstr[64];
-	int n;
-	Tm gmt, *atm;
+	char **f, *fmts[] = {
+		"?W, ?DD ?MMM YYYY hh:mm:ss ?Z",
+		"?W ?M ?DD hh:mm:ss ?Z YYYY",
+		"?W, DD-?MM-YY hh:mm:ss ?Z",
+		"?DD ?MMM YYYY hh:mm:ss ?Z",
+		"?M ?DD hh:mm:ss ?Z YYYY",
+		"DD-?MM-YYYY hh:mm:ss ?Z",
+		nil,
+	};
 
-	/*
-	 * default date is Thu Jan  1 00:00:00 GMT 1970
-	 */
-	tm->wday = 4;
-	tm->mday = 1;
-	tm->mon = 1;
-	tm->hour = 0;
-	tm->min = 0;
-	tm->sec = 0;
-	tm->year = 70;
-	strcpy(tm->zone, "GMT");
-	tm->tzoff = 0;
-
-	strncpy(dstr, date, sizeof dstr);
-	dstr[sizeof dstr - 1] = 0;
-	n = tokenize(dstr, flds, 7);
-	if(n != 6 && n != 5)
-		return nil;
-
-	if(n == 5){
-		for(n = 5; n >= 1; n--)
-			flds[n] = flds[n - 1];
-		n = 5;
-	}else{
-		/*
-		 * Wday[,]
-		 */
-		s = strchr(flds[0], ',');
-		if(s != nil)
-			*s = 0;
-		tm->wday = dateindex(flds[0], wdayname, 7);
-		if(tm->wday < 0)
-			return nil;
-	}
-
-	/*
-	 * check for the two major formats:
-	 * Month first or day first
-	 */
-	tm->mon = dateindex(flds[1], monname, 12);
-	if(tm->mon >= 0){
-		tm->mday = strtoul(flds[2], nil, 10);
-		time2tm(tm, flds[3]);
-		zone2tm(tm, flds[4]);
-		tm->year = strtoul(flds[5], nil, 10);
-		if(strlen(flds[5]) > 2)
-			tm->year -= 1900;
-	}else{
-		tm->mday = strtoul(flds[1], nil, 10);
-		tm->mon = dateindex(flds[2], monname, 12);
-		if(tm->mon < 0)
-			return nil;
-		tm->year = strtoul(flds[3], nil, 10);
-		if(strlen(flds[3]) > 2)
-			tm->year -= 1900;
-		time2tm(tm, flds[4]);
-		zone2tm(tm, flds[5]);
-	}
-
-	if(n == 5){
-		gmt = *tm;
-		strncpy(gmt.zone, "", 4);
-		gmt.tzoff = 0;
-		atm = gmtime(tm2sec(&gmt));
-		tm->wday = atm->wday;
-	}else{
-		/*
-		 * Wday[,]
-		 */
-		s = strchr(flds[0], ',');
-		if(s != nil)
-			*s = 0;
-		tm->wday = dateindex(flds[0], wdayname, 7);
-		if(tm->wday < 0)
-			return nil;
-	}
-	return tm;
+	for(f = fmts; *f; f++)
+		if(tmparse(tm, *f, date, nil, nil) != nil)
+			return tm;
+	return nil;
 }
diff -r c5110aa667d8 sys/src/cmd/upas/imap4d/imap4d.c
--- a/sys/src/cmd/upas/imap4d/imap4d.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/imap4d/imap4d.c	Fri Jul 31 08:22:43 2020 -0700
@@ -215,6 +215,7 @@
 	Binit(&bin, dup(0, -1), OREAD);
 	close(0);
 	Binit(&bout, 1, OWRITE);
+	tmfmtinstall();
 	quotefmtinstall();
 	fmtinstall('F', Ffmt);
 	fmtinstall('D', Dfmt);	/* rfc822; # imap date %Z */
diff -r c5110aa667d8 sys/src/cmd/upas/imap4d/print.c
--- a/sys/src/cmd/upas/imap4d/print.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/imap4d/print.c	Fri Jul 31 08:22:43 2020 -0700
@@ -90,40 +90,25 @@
 	return fmtstrcpy(f, encfs(buf, sizeof buf, s));
 }
 
-static char *day[] = {
-	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
-};
-
-static char *mon[] = {
-	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
-};
-
 int
 Dfmt(Fmt *f)
 {
-	char buf[128], *p, *e, *sgn, *fmt;
-	int off;
-	Tm *tm;
+	char buf[128], *fmt;
+	Tm *tm, t;
+	Tzone *tz;
 
 	tm = va_arg(f->args, Tm*);
-	if(tm == nil)
-		tm = localtime(time(0));
-	sgn = "+";
-	if(tm->tzoff < 0)
-		sgn = "";
-	e = buf + sizeof buf;
-	p = buf;
-	off = (tm->tzoff/3600)*100 + (tm->tzoff/60)%60;
+	if(tm == nil){
+		tz = tzload("local");
+		tm = tmtime(&t, time(0), tz);
+	}
 	if((f->flags & FmtSharp) == 0){
 		/* rfc822 style */
-		fmt = "%.2d %s %.4d %.2d:%.2d:%.2d %s%.4d";
-		p = seprint(p, e, "%s, ", day[tm->wday]);
+		fmt = "WW, DD MMM YYYY hh:mm:ss Z";
 	}else
-		fmt = "%2d-%s-%.4d %2.2d:%2.2d:%2.2d %s%4.4d";
-	seprint(p, e, fmt,
-		tm->mday, mon[tm->mon], tm->year + 1900, tm->hour, tm->min, tm->sec,
-		sgn, off);
+		fmt = "DD-MMM-YYYY hh:mm:ss Z";
 	if(f->r == L'δ')
-		return fmtstrcpy(f, buf);
+		return fmtprint(f, "%τ", tmfmt(tm, fmt));
+	snprint(buf, sizeof(buf), "%τ", tmfmt(tm, fmt));
 	return fmtprint(f, "%Z", buf);
 }
diff -r c5110aa667d8 sys/src/cmd/upas/marshal/marshal.c
--- a/sys/src/cmd/upas/marshal/marshal.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/cmd/upas/marshal/marshal.c	Fri Jul 31 08:22:43 2020 -0700
@@ -140,6 +140,7 @@
 char lastchar;
 char *replymsg;
 
+#define Rfc822fmt	"WW, DD MMM YYYY hh:mm:ss Z"
 enum
 {
 	Ok = 0,
@@ -208,6 +209,7 @@
 	hdrstring = nil;
 	ccargc = bccargc = 0;
 
+	tmfmtinstall();
 	quotefmtinstall();
 	fmtinstall('Z', doublequote);
 	fmtinstall('U', rfc2047fmt);
@@ -792,29 +794,13 @@
 	Bterm(f);
 }
 
-char *ascwday[] =
-{
-	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
-};
-
-char *ascmon[] =
-{
-	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
-	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
-};
-
 int
 printdate(Biobuf *b)
 {
-	int tz;
 	Tm *tm;
 
 	tm = localtime(time(0));
-	tz = (tm->tzoff/3600)*100 + (tm->tzoff/60)%60;
-
-	return Bprint(b, "Date: %s, %d %s %d %2.2d:%2.2d:%2.2d %s%.4d\n",
-		ascwday[tm->wday], tm->mday, ascmon[tm->mon], 1900 + tm->year,
-		tm->hour, tm->min, tm->sec, tz>=0?"+":"", tz);
+	return Bprint(b, "Date: %τ\n", tmfmt(tm, Rfc822fmt));
 }
 
 int
@@ -1003,16 +989,10 @@
 int
 printunixfrom(int fd)
 {
-	int tz;
 	Tm *tm;
 
 	tm = localtime(time(0));
-	tz = (tm->tzoff/3600)*100 + (tm->tzoff/60)%60;
-
-	return fprint(fd, "From %s %s %s %d %2.2d:%2.2d:%2.2d %s%.4d %d\n",
-		user,
-		ascwday[tm->wday], ascmon[tm->mon], tm->mday,
-		tm->hour, tm->min, tm->sec, tz>=0?"+":"", tz, 1900 + tm->year);
+	return fprint(fd, "From %s %τ\n", user, tmfmt(tm, Rfc822fmt));
 }
 
 char *specialfile[] =
diff -r c5110aa667d8 sys/src/libc/9sys/ctime.c
--- a/sys/src/libc/9sys/ctime.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/libc/9sys/ctime.c	Fri Jul 31 08:22:43 2020 -0700
@@ -1,301 +1,39 @@
-/*
- * This routine converts time as follows.
- * The epoch is 0000 Jan 1 1970 GMT.
- * The argument time is in seconds since then.
- * The localtime(t) entry returns a pointer to an array
- * containing
- *
- *	seconds (0-59)
- *	minutes (0-59)
- *	hours (0-23)
- *	day of month (1-31)
- *	month (0-11)
- *	year-1970
- *	weekday (0-6, Sun is 0)
- *	day of the year
- *	daylight savings flag
- *
- * The routine gets the daylight savings time from the environment.
- *
- * asctime(tvec))
- * where tvec is produced by localtime
- * returns a ptr to a character string
- * that has the ascii time in the form
- *
- *	                            \\
- *	Thu Jan 01 00:00:00 GMT 1970n0
- *	012345678901234567890123456789
- *	0	  1	    2
- *
- * ctime(t) just calls localtime, then asctime.
- */
-
 #include <u.h>
 #include <libc.h>
 
-static	char	dmsize[12] =
-{
-	31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
-};
-
-/*
- * The following table is used for 1974 and 1975 and
- * gives the day number of the first day after the Sunday of the
- * change.
- */
-
-static	int	dysize(int);
-static	void	ct_numb(char*, int);
-
-#define	TZSIZE	150
-static	void	readtimezone(void);
-static	int	rd_name(char**, char*);
-static	int	rd_long(char**, long*);
-static
-struct
-{
-	char	stname[4];
-	char	dlname[4];
-	long	stdiff;
-	long	dldiff;
-	long	dlpairs[TZSIZE];
-} timezone;
-
-char*
-ctime(long t)
-{
-	return asctime(localtime(t));
-}
-
 Tm*
 localtime(long tim)
 {
-	Tm *ct;
-	long t, *p;
-	int dlflag;
+	static Tm tm;
+	Tzone *tz;
 
-	if(timezone.stname[0] == 0)
-		readtimezone();
-	t = tim + timezone.stdiff;
-	dlflag = 0;
-	for(p = timezone.dlpairs; *p; p += 2)
-		if(t >= p[0])
-		if(t < p[1]) {
-			t = tim + timezone.dldiff;
-			dlflag++;
-			break;
-		}
-	ct = gmtime(t);
-	if(dlflag){
-		strcpy(ct->zone, timezone.dlname);
-		ct->tzoff = timezone.dldiff;
-	} else {
-		strcpy(ct->zone, timezone.stname);
-		ct->tzoff = timezone.stdiff;
-	}
-	return ct;
+	/*
+	 * We have no way to report errors,
+	 * so we just ignore them here.
+	 */
+	tz = tzload("local");
+	tmtime(&tm, tim, tz);
+	return &tm;
 }
 
 Tm*
-gmtime(long tim)
+gmtime(long abs)
 {
-	int d0, d1;
-	long hms, day;
-	static Tm xtime;
-
-	/*
-	 * break initial number into days
-	 */
-	hms = tim % 86400L;
-	day = tim / 86400L;
-	if(hms < 0) {
-		hms += 86400L;
-		day -= 1;
-	}
-
-	/*
-	 * generate hours:minutes:seconds
-	 */
-	xtime.sec = hms % 60;
-	d1 = hms / 60;
-	xtime.min = d1 % 60;
-	d1 /= 60;
-	xtime.hour = d1;
-
-	/*
-	 * day is the day number.
-	 * generate day of the week.
-	 * The addend is 4 mod 7 (1/1/1970 was Thursday)
-	 */
-
-	xtime.wday = (day + 7340036L) % 7;
-
-	/*
-	 * year number
-	 */
-	if(day >= 0)
-		for(d1 = 1970; day >= dysize(d1); d1++)
-			day -= dysize(d1);
-	else
-		for (d1 = 1970; day < 0; d1--)
-			day += dysize(d1-1);
-	xtime.year = d1-1900;
-	xtime.yday = d0 = day;
-
-	/*
-	 * generate month
-	 */
-
-	if(dysize(d1) == 366)
-		dmsize[1] = 29;
-	for(d1 = 0; d0 >= dmsize[d1]; d1++)
-		d0 -= dmsize[d1];
-	dmsize[1] = 28;
-	xtime.mday = d0 + 1;
-	xtime.mon = d1;
-	strcpy(xtime.zone, "GMT");
-	return &xtime;
+	static Tm tm;
+	return tmtime(&tm, abs, nil);
 }
 
 char*
-asctime(Tm *t)
+ctime(long abs)
 {
-	char *ncp;
-	static char cbuf[30];
+	Tzone *tz;
+	Tm tm;
 
-	strcpy(cbuf, "Thu Jan 01 00:00:00 GMT 1970\n");
-	ncp = &"SunMonTueWedThuFriSat"[t->wday*3];
-	cbuf[0] = *ncp++;
-	cbuf[1] = *ncp++;
-	cbuf[2] = *ncp;
-	ncp = &"JanFebMarAprMayJunJulAugSepOctNovDec"[t->mon*3];
-	cbuf[4] = *ncp++;
-	cbuf[5] = *ncp++;
-	cbuf[6] = *ncp;
-	ct_numb(cbuf+8, t->mday);
-	ct_numb(cbuf+11, t->hour+100);
-	ct_numb(cbuf+14, t->min+100);
-	ct_numb(cbuf+17, t->sec+100);
-	ncp = t->zone;
-	cbuf[20] = *ncp++;
-	cbuf[21] = *ncp++;
-	cbuf[22] = *ncp;
-	ct_numb(cbuf+24, (t->year+1900) / 100 + 100);
-	ct_numb(cbuf+26, t->year+100);
-	return cbuf;
+	/*
+	 * We have no way to report errors,
+	 * so we just ignore them here.
+	 */
+	tz = tzload("local");
+	tmtime(&tm, abs, tz);
+	return asctime(&tm);
 }
-
-static
-dysize(int y)
-{
-
-	if(y%4 == 0 && (y%100 != 0 || y%400 == 0))
-		return 366;
-	return 365;
-}
-
-static
-void
-ct_numb(char *cp, int n)
-{
-
-	cp[0] = ' ';
-	if(n >= 10)
-		cp[0] = (n/10)%10 + '0';
-	cp[1] = n%10 + '0';
-}
-
-static
-void
-readtimezone(void)
-{
-	char buf[TZSIZE*11+30], *p;
-	int i;
-
-	memset(buf, 0, sizeof(buf));
-	i = open("/env/timezone", 0);
-	if(i < 0)
-		goto error;
-	if(read(i, buf, sizeof(buf)) >= sizeof(buf)){
-		close(i);
-		goto error;
-	}
-	close(i);
-	p = buf;
-	if(rd_name(&p, timezone.stname))
-		goto error;
-	if(rd_long(&p, &timezone.stdiff))
-		goto error;
-	if(rd_name(&p, timezone.dlname))
-		goto error;
-	if(rd_long(&p, &timezone.dldiff))
-		goto error;
-	for(i=0; i<TZSIZE; i++) {
-		if(rd_long(&p, &timezone.dlpairs[i]))
-			goto error;
-		if(timezone.dlpairs[i] == 0)
-			return;
-	}
-
-error:
-	timezone.stdiff = 0;
-	strcpy(timezone.stname, "GMT");
-	timezone.dlpairs[0] = 0;
-}
-
-static
-rd_name(char **f, char *p)
-{
-	int c, i;
-
-	for(;;) {
-		c = *(*f)++;
-		if(c != ' ' && c != '\n')
-			break;
-	}
-	for(i=0; i<3; i++) {
-		if(c == ' ' || c == '\n')
-			return 1;
-		*p++ = c;
-		c = *(*f)++;
-	}
-	if(c != ' ' && c != '\n')
-		return 1;
-	*p = 0;
-	return 0;
-}
-
-static
-rd_long(char **f, long *p)
-{
-	int c, s;
-	long l;
-
-	s = 0;
-	for(;;) {
-		c = *(*f)++;
-		if(c == '-') {
-			s++;
-			continue;
-		}
-		if(c != ' ' && c != '\n')
-			break;
-	}
-	if(c == 0) {
-		*p = 0;
-		return 0;
-	}
-	l = 0;
-	for(;;) {
-		if(c == ' ' || c == '\n')
-			break;
-		if(c < '0' || c > '9')
-			return 1;
-		l = l*10 + c-'0';
-		c = *(*f)++;
-	}
-	if(s)
-		l = -l;
-	*p = l;
-	return 0;
-}
diff -r c5110aa667d8 sys/src/libc/9sys/tm2sec.c
--- a/sys/src/libc/9sys/tm2sec.c	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/libc/9sys/tm2sec.c	Fri Jul 31 08:22:43 2020 -0700
@@ -1,202 +1,11 @@
 #include <u.h>
 #include <libc.h>
 
-#define	TZSIZE	150
-static	void	readtimezone(void);
-static	int	rd_name(char**, char*);
-static	int	rd_long(char**, long*);
-static
-struct
-{
-	char	stname[4];
-	char	dlname[4];
-	long	stdiff;
-	long	dldiff;
-	long	dlpairs[TZSIZE];
-} timezone;
-
-#define SEC2MIN 60L
-#define SEC2HOUR (60L*SEC2MIN)
-#define SEC2DAY (24L*SEC2HOUR)
-
-/*
- *  days per month plus days/year
- */
-static	int	dmsize[] =
-{
-	365, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
-};
-static	int	ldmsize[] =
-{
-	366, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
-};
-
-/*
- *  return the days/month for the given year
- */
-static int *
-yrsize(int y)
-{
-	if((y%4) == 0 && ((y%100) != 0 || (y%400) == 0))
-		return ldmsize;
-	else
-		return dmsize;
-}
-
-/*
- * compute seconds since Jan 1 1970 GMT
- * and convert to our timezone.
- */
 long
 tm2sec(Tm *tm)
 {
-	long secs, *p;
-	int i, yday, year, *d2m;
+	Tm tt;
 
-	if(strcmp(tm->zone, "GMT") != 0 && timezone.stname[0] == 0)
-		readtimezone();
-	secs = 0;
-
-	/*
-	 *  seconds per year
-	 */
-	year = tm->year + 1900;
-	for(i = 1970; i < year; i++){
-		d2m = yrsize(i);
-		secs += d2m[0] * SEC2DAY;
-	}
-
-	/*
-	 *  if mday is set, use mon and mday to compute yday
-	 */
-	if(tm->mday){
-		yday = 0;
-		d2m = yrsize(year);
-		for(i=0; i<tm->mon; i++)
-			yday += d2m[i+1];
-		yday += tm->mday-1;
-	}else{
-		yday = tm->yday;
-	}
-	secs += yday * SEC2DAY;
-
-	/*
-	 * hours, minutes, seconds
-	 */
-	secs += tm->hour * SEC2HOUR;
-	secs += tm->min * SEC2MIN;
-	secs += tm->sec;
-
-	/*
-	 * Only handles zones mentioned in /env/timezone,
-	 * but things get too ambiguous otherwise.
-	 */
-	if(strcmp(tm->zone, timezone.stname) == 0)
-		secs -= timezone.stdiff;
-	else if(strcmp(tm->zone, timezone.dlname) == 0)
-		secs -= timezone.dldiff;
-	else if(tm->zone[0] == 0){
-		secs -= timezone.dldiff;
-		for(p = timezone.dlpairs; *p; p += 2)
-			if(secs >= p[0] && secs < p[1])
-				break;
-		if(*p == 0){
-			secs += timezone.dldiff;
-			secs -= timezone.stdiff;
-		}
-	}
-	return secs;
+	tt = *tm;
+	return tmnorm(&tt);
 }
-
-static
-void
-readtimezone(void)
-{
-	char buf[TZSIZE*11+30], *p;
-	int i;
-
-	memset(buf, 0, sizeof(buf));
-	i = open("/env/timezone", 0);
-	if(i < 0)
-		goto error;
-	if(read(i, buf, sizeof(buf)) >= sizeof(buf))
-		goto error;
-	close(i);
-	p = buf;
-	if(rd_name(&p, timezone.stname))
-		goto error;
-	if(rd_long(&p, &timezone.stdiff))
-		goto error;
-	if(rd_name(&p, timezone.dlname))
-		goto error;
-	if(rd_long(&p, &timezone.dldiff))
-		goto error;
-	for(i=0; i<TZSIZE; i++) {
-		if(rd_long(&p, &timezone.dlpairs[i]))
-			goto error;
-		if(timezone.dlpairs[i] == 0)
-			return;
-	}
-
-error:
-	timezone.stdiff = 0;
-	strcpy(timezone.stname, "GMT");
-	timezone.dlpairs[0] = 0;
-}
-
-static int
-rd_name(char **f, char *p)
-{
-	int c, i;
-
-	for(;;) {
-		c = *(*f)++;
-		if(c != ' ' && c != '\n')
-			break;
-	}
-	for(i=0; i<3; i++) {
-		if(c == ' ' || c == '\n')
-			return 1;
-		*p++ = c;
-		c = *(*f)++;
-	}
-	if(c != ' ' && c != '\n')
-		return 1;
-	*p = 0;
-	return 0;
-}
-
-static int
-rd_long(char **f, long *p)
-{
-	int c, s;
-	long l;
-
-	s = 0;
-	for(;;) {
-		c = *(*f)++;
-		if(c == '-') {
-			s++;
-			continue;
-		}
-		if(c != ' ' && c != '\n')
-			break;
-	}
-	if(c == 0) {
-		*p = 0;
-		return 0;
-	}
-	l = 0;
-	for(;;) {
-		if(c == ' ' || c == '\n')
-			break;
-		if(c < '0' || c > '9')
-			return 1;
-		l = l*10 + c-'0';
-		c = *(*f)++;
-	}
-	if(s)
-		l = -l;
-	*p = l;
-	return 0;
-}
diff -r c5110aa667d8 sys/src/libc/port/date.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sys/src/libc/port/date.c	Fri Jul 31 08:22:43 2020 -0700
@@ -0,0 +1,996 @@
+#include <u.h>
+#include <libc.h>
+
+typedef struct Tzabbrev Tzabbrev;
+typedef struct Tzoffpair Tzoffpair;
+
+#define Ctimefmt	"WW MMM _D hh:mm:ss ZZZ YYYY"
+#define P(pad, w)	((pad) < (w) ? 0 : pad - w)
+
+enum {
+	Tzsize		= 150,
+	Nsec		= 1000*1000*1000,
+	Usec		= 1000*1000,
+	Msec		= 1000,
+	Daysec		= (vlong)24*3600,
+	Days400y	= 365*400 + 4*25 - 3,
+	Days4y		= 365*4 + 1,
+};
+
+enum {
+	Cend,
+	Cspace,
+	Cnum,
+	Cletter,
+	Cpunct,
+};
+	
+struct Tzone {
+	char	tzname[32];
+	char	stname[16];
+	char	dlname[16];
+	long	stdiff;
+	long	dldiff;
+	long	dlpairs[150];
+};
+
+static QLock zlock;
+static int nzones;
+static Tzone **zones;
+static int mdays[] = {
+	31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+};
+static char *wday[] = {
+	"Sunday","Monday","Tuesday",
+	"Wednesday","Thursday","Friday",
+	"Saturday", nil,
+};
+static char *month[] = {
+	"January", "February", "March",
+	"April", "May", "June", "July",
+	"August", "September", "October",
+	"November", "December", nil
+};
+
+struct Tzabbrev {
+	char *abbr;
+	char *name;
+};
+
+struct Tzoffpair {
+	char *abbr;
+	int off;
+};
+
+#define isalpha(c)\
+	(((c)|0x60) >= 'a' && ((c)|0x60) <= 'z')
+
+/* Obsolete time zone names. Hardcoded to match RFC5322 */
+static Tzabbrev tzabbrev[] = {
+	{"UT", "GMT"}, {"GMT", "GMT"}, {"UTC", "GMT"},
+	{"EST",	"US_Eastern"}, {"EDT", "US_Eastern"},
+	{"CST", "US_Central"}, {"CDT", "US_Central"},
+	{"MST", "US_Mountain"}, {"MDT", "US_Mountain"},
+	{"PST", "US_Pacific"}, {"PDT", "US_Pacific"},
+	{nil},
+};
+
+/* Military timezone names */
+static Tzoffpair milabbrev[] = {
+	{"A", -1*3600},   {"B", -2*3600},   {"C", -3*3600},
+	{"D", -4*3600},   {"E", -5*3600},   {"F", -6*3600},
+	{"G", -7*3600},   {"H", -8*3600},   {"I", -9*3600},
+	{"K", -10*3600},  {"L", -11*3600},  {"M", -12*3600},
+	{"N", +1*3600},   {"O", +2*3600},   {"P", +3*3600},
+	{"Q", +4*3600},   {"R", +5*3600},   {"S", +6*3600},
+	{"T", +7*3600},   {"U", +8*3600},   {"V", +9*3600},
+	{"W", +10*3600},  {"X", +11*3600}, {"Y", +12*3600},
+	{"Z",	0}, {nil, 0}
+};
+
+static vlong
+mod(vlong a, vlong b)
+{
+	vlong r;
+
+	r = a % b;
+	if(r < 0)
+		r += b;
+	return r;
+}
+
+static int
+isleap(int y)
+{
+	return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
+}
+
+static int
+rdname(char **f, char *p, int n)
+{
+	char *s, *e;
+
+	for(s = *f; *s; s++)
+		if(*s != ' ' && *s != '\t'  && *s != '\n')
+			break;
+	e = s + n;
+	for(; *s && s != e; s++) {
+		if(*s == ' ' || *s == '\t' || *s == '\n')
+			break;
+		*p++ = *s;
+	}
+	*p = 0;
+	if(n - (e - s) < 3 || *s != ' ' && *s != '\t' && *s != '\n'){
+		werrstr("truncated name");
+		return -1;
+	}
+	*f = s;
+	return 0;
+}
+
+static int
+rdlong(char **f, long *p)
+{
+	int c, s;
+	long l;
+
+	s = 0;
+	while((c = *(*f)++) != 0){
+		if(c == '-')
+			s++;
+		else if(c != ' ' && c != '\n')
+			break;
+	}
+	if(c == 0) {
+		*p = 0;
+		return 0;
+	}
+	l = 0;
+	for(;;) {
+		if(c == ' ' || c == '\n')
+			break;
+		if(c < '0' || c > '9'){
+			werrstr("non-number %c in name", c);
+			return -1;
+		}
+		l = l*10 + c-'0';
+		c = *(*f)++;
+	}
+	if(s)
+		l = -l;
+	*p = l;
+	return 0;
+}
+
+static int
+loadzone(Tzone *tz, char *name)
+{
+	char buf[Tzsize*11+30], path[128], *p;
+	int i, f, r;
+
+	memset(tz, 0, sizeof(Tzone));
+	if(strcmp(name, "local") == 0)
+		snprint(path, sizeof(path), "/env/timezone");
+	else
+		snprint(path, sizeof(path), "/adm/timezone/%s", name);
+	memset(buf, 0, sizeof(buf));
+	if((f = open(path, 0)) == -1)
+		return -1;
+	r = read(f, buf, sizeof(buf));
+	close(f);
+	if(r == sizeof(buf) || r == -1)
+		return -1;
+	buf[r] = 0;
+	p = buf;
+	if(rdname(&p, tz->stname, sizeof(tz->stname)) == -1)
+		return -1;
+	if(rdlong(&p, &tz->stdiff) == -1)
+		return -1;
+	if(rdname(&p, tz->dlname, sizeof(tz->dlname)) == -1)
+		return -1;
+	if(rdlong(&p, &tz->dldiff) == -1)
+		return -1;
+	for(i=0; i < Tzsize; i++) {
+		if(rdlong(&p, &tz->dlpairs[i]) == -1){
+			werrstr("invalid transition time");
+			return -1;
+		}
+		if(tz->dlpairs[i] == 0)
+			return 0;
+	}
+	werrstr("invalid timezone %s", name);
+	return -1;
+}
+
+Tzone*
+tzload(char *tzname)
+{
+	Tzone *tz, **newzones;
+	int i;
+
+	if(tzname == nil)
+		tzname = "GMT";
+	qlock(&zlock);
+	for(i = 0; i < nzones; i++){
+		tz = zones[i];
+		if(strcmp(tz->stname, tzname) == 0)
+			goto found;
+		if(strcmp(tz->dlname, tzname) == 0)
+			goto found;
+		if(strcmp(tz->tzname, tzname) == 0)
+			goto found;
+	}
+
+	tz = malloc(sizeof(Tzone));
+	if(tz == nil)
+		goto error;
+	newzones = realloc(zones, (nzones + 1) * sizeof(Tzone*));
+	if(newzones == nil)
+		goto error;
+	if(loadzone(tz, tzname) != 0)
+		goto error;
+	if(snprint(tz->tzname, sizeof(tz->tzname), tzname) >= sizeof(tz->tzname)){
+		werrstr("timezone name too long");
+		return nil;
+	}
+	zones = newzones;
+	zones[nzones] = tz;
+	nzones++;
+found:
+	qunlock(&zlock);
+	return tz;
+error:
+	free(tz);
+	qunlock(&zlock);
+	return nil;
+}
+
+static void
+tzoffset(Tzone *tz, vlong abs, Tm *tm)
+{
+	long dl, *p;
+	dl = 0;
+	if(tz == nil){
+		snprint(tm->zone, sizeof(tm->zone), "GMT");
+		tm->tzoff = 0;
+		return;
+	}
+	for(p = tz->dlpairs; *p; p += 2)
+		if(abs > p[0] && abs <= p[1]){
+			dl = 1;
+			break;
+		}
+	if(dl){
+		snprint(tm->zone, sizeof(tm->zone), tz->dlname);
+		tm->tzoff = tz->dldiff;
+	}else{
+		snprint(tm->zone, sizeof(tm->zone), tz->stname);
+		tm->tzoff = tz->stdiff;
+	}
+}
+
+static Tm*
+tmfill(Tm *tm, vlong abs, vlong nsec)
+{
+	vlong zrel, j, y, m, d, t, e;
+	int i;
+
+	zrel = abs + tm->tzoff;
+	t = zrel % Daysec;
+	e = zrel / Daysec;
+	if(t < 0){
+		t += Daysec;
+		e -= 1;
+	}
+
+	t += nsec/Nsec;
+	tm->sec = mod(t, 60);
+	t /= 60;
+	tm->min = mod(t, 60);
+	t /= 60;
+	tm->hour = mod(t, 24);
+	tm->wday = mod((e + 4), 7);
+
+	/*
+	 * Split up year, month, day.
+	 * 
+	 * Implemented according to "Algorithm 199,
+	 * conversions between calendar  date and
+	 * Julian day number", Robert G. Tantzen,
+	 * Air Force Missile Development
+	 * Center, Holloman AFB, New Mex.
+	 * 
+	 * Lots of magic.
+	 */
+	j = (zrel + 2440588 * Daysec) / (Daysec) - 1721119;
+	y = (4 * j - 1) / Days400y;
+	j = 4 * j - 1 - Days400y * y;
+	d = j / 4;
+	j = (4 * d + 3) / Days4y;
+	d = 4 * d + 3 - Days4y * j;
+	d = (d + 4) / 4 ;
+	m = (5 * d - 3) / 153;
+	d = 5 * d - 3 - 153 * m;
+	d = (d + 5) / 5;
+	y = 100 * y + j;
+
+	if(m < 10)
+		m += 3;
+	else{
+		m -= 9;
+		y++;
+	}
+
+	/* there's no year 0 */
+	if(y <= 0)
+		y--;
+	/* and if j negative, the day and month are also negative */
+	if(m < 0)
+		m += 12;
+	if(d < 0)
+		d += mdays[m - 1];
+
+	tm->yday = d;
+	for(i = 0; i < m - 1; i++)
+		tm->yday += mdays[i];
+	if(m > 1 && isleap(y))
+		tm->yday++;
+	tm->year = y - 1900;
+	tm->mon = m - 1;
+	tm->mday = d;
+	tm->nsec = mod(nsec, Nsec);
+	return tm;
+}	
+
+
+Tm*
+tmtime(Tm *tm, vlong abs, Tzone *tz)
+{
+	return tmtimens(tm, abs, 0, tz);
+}
+
+Tm*
+tmtimens(Tm *tm, vlong abs, int ns, Tzone *tz)
+{
+	tm->tz = tz;
+	tzoffset(tz, abs, tm);
+	return tmfill(tm, abs, ns);
+}
+
+Tm*
+tmnow(Tm *tm, Tzone *tz)
+{
+	vlong ns;
+
+	ns = nsec();
+	return tmtimens(tm, nsec()/Nsec, mod(ns, Nsec), tz);
+}
+
+vlong
+tmnorm(Tm *tm)
+{
+	vlong c, yadj, j, abs, y, m, d;
+
+	if(tm->mon > 1){
+		m = tm->mon - 2;
+		y = tm->year + 1900;
+	}else{
+		m = tm->mon + 10;
+		y = tm->year + 1899;
+	}
+	d = tm->mday;
+	c = y / 100;
+	yadj = y - 100 * c;
+	j = (c * Days400y / 4 + 
+		Days4y * yadj / 4 +
+		(153 * m + 2)/5 + d -
+		719469);
+	abs = j * Daysec;
+	abs += tm->hour * 3600;
+	abs += tm->min * 60;
+	abs += tm->sec;
+	if(tm->tz){
+		tzoffset(tm->tz, abs - tm->tzoff, tm);
+		tzoffset(tm->tz, abs - tm->tzoff, tm);
+	}
+	abs -= tm->tzoff;
+	tmfill(tm, abs, tm->nsec);
+	return abs;
+}
+
+static int
+τconv(Fmt *f)
+{
+	int depth, n, v, w, h, m, c0, sgn, pad, off;
+	char *p, *am;
+	Tmfmt tf;
+	Tm *tm;
+
+	n = 0;
+	tf = va_arg(f->args, Tmfmt);
+	tm = tf.tm;
+	p = tf.fmt;
+	if(p == nil)
+		p = Ctimefmt;
+	while(*p){
+		w = 1;
+		pad = 0;
+		while(*p == '_'){
+			pad++;
+			p++;
+		}
+		c0 = *p++;
+		while(c0 && *p == c0){
+			w++;
+			p++;
+		}
+		pad += w;
+		switch(c0){
+		case 0:
+			break;
+		case 'Y':
+			switch(w){
+			case 1:	n += fmtprint(f, "%*d", pad, tm->year + 1900);		break;
+			case 2: n += fmtprint(f, "%*d", pad, tm->year % 100);		break;
+			case 4:	n += fmtprint(f, "%*d", pad, tm->year + 1900);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 'M':
+			switch(w){
+			case 1: n += fmtprint(f, "%*d", pad, tm->mon + 1);		break;
+			case 2:	n += fmtprint(f, "%*s%02d", pad-2, "", tm->mon + 1);	break;
+			case 3:	n += fmtprint(f, "%*.3s", pad, month[tm->mon]);		break;
+			case 4:	n += fmtprint(f, "%*s", pad, month[tm->mon]);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 'D':
+			switch(w){
+			case 1: n += fmtprint(f, "%*d", pad, tm->mday);			break;
+			case 2:	n += fmtprint(f, "%*s%02d", pad-2, "", tm->mday);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 'W':
+			switch(w){
+			case 1:	n += fmtprint(f, "%*d", pad, tm->wday + 1);		break;
+			case 2:	n += fmtprint(f, "%*.3s", pad, wday[tm->wday]);		break;
+			case 3:	n += fmtprint(f, "%*s", pad, wday[tm->wday]);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 'H':
+			switch(w){
+			case 1: n += fmtprint(f, "%*d", pad, tm->hour % 12);		break;
+			case 2:	n += fmtprint(f, "%*s%02d", pad-2, "", tm->hour % 12);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 'h':
+			switch(w){
+			case 1: n += fmtprint(f, "%*d", pad, tm->hour);			break;
+			case 2:	n += fmtprint(f, "%*s%02d", pad-2, "", tm->hour);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 'm':
+			switch(w){
+			case 1: n += fmtprint(f, "%*d", pad, tm->min);			break;
+			case 2:	n += fmtprint(f, "%*s%02d", pad-2, "", tm->min);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 's':
+			switch(w){
+			case 1: n += fmtprint(f, "%*d", pad, tm->sec);			break;
+			case 2:	n += fmtprint(f, "%*s%02d", pad-2, "", tm->sec);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 't':
+			v = tm->nsec / (1000*1000);
+			switch(w){
+			case 1:	n += fmtprint(f, "%*d", pad, v % 1000);			break;
+			case 2:
+			case 3:	n += fmtprint(f, "%*s%03d", P(pad, 3), "", v % 1000);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 'u':
+			v = tm->nsec / 1000;
+			switch(w){
+			case 1:	n += fmtprint(f, "%*d", pad, v % 1000);			break;
+			case 2:	n += fmtprint(f, "%*s%03d", P(pad, 3), "", v % 1000);	break;
+			case 3:	n += fmtprint(f, "%*d", P(pad, 6), v);			break;
+			case 4:	n += fmtprint(f, "%*s%06d", P(pad, 6), "", v);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 'n':
+			v = tm->nsec;
+			switch(w){
+			case 1:	n += fmtprint(f, "%*d", pad, v%1000);			break;
+			case 2:	n += fmtprint(f, "%*s%03d", P(pad, 3), "", v % 1000);	break;
+			case 3:	n += fmtprint(f, "%*d", pad , v%(1000*1000));		break;
+			case 4: n += fmtprint(f, "%*s%06d", P(pad, 6), "", v%(1000000)); break;
+			case 5:	n += fmtprint(f, "%*d", pad, v);			break;
+			case 6:	n += fmtprint(f, "%*s%09d", P(pad, 9), "", v);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 'z':
+			if(w != 1)
+				goto badfmt;
+		case 'Z':
+			sgn = (tm->tzoff < 0) ? '-' : '+';
+			off = (tm->tzoff < 0) ? -tm->tzoff : tm->tzoff;
+			h = off/3600;
+			m = (off/60)%60;
+			if(w < 3 && pad < 5)
+				pad = 5;
+			switch(w){
+			case 1:	n += fmtprint(f, "%*s%c%02d%02d", pad-5, "", sgn, h, m); break;
+			case 2:	n += fmtprint(f, "%*s%c%02d:%02d", pad-5, "", sgn, h, m); break;
+			case 3:	n += fmtprint(f, "%*s", pad, tm->zone);			 break;
+			}
+			break;
+		case 'A':
+		case 'a':
+			if(w != 1)
+				goto badfmt;
+			if(c0 == 'a')
+				am = (tm->hour < 12) ? "am" : "pm";
+			else
+				am = (tm->hour < 12) ? "AM" : "PM";
+			n += fmtprint(f, "%*s", pad, am);
+			break;
+		case '[':
+			depth = 1;
+			while(*p){
+				if(*p == '[')
+					depth++;
+				if(*p == ']')
+					depth--;
+				if(*p == '\\')
+					p++;
+				if(depth == 0)
+					break;
+				fmtrune(f, *p++);
+			}
+			if(*p++ != ']')
+				goto badfmt;
+			break;
+		default:
+			while(w-- > 0)
+				n += fmtrune(f, c0);
+			break;
+		}
+	}
+	return n;
+badfmt:
+	werrstr("garbled format %s", tf.fmt);
+	return -1;			
+}
+
+static int
+getnum(char **ps, int maxw, int *ok)
+{
+	char *s, *e;
+	int n;
+
+	n = 0;
+	e = *ps + maxw;
+	for(s = *ps; s != e && *s >= '0' && *s <= '9'; s++){
+		n *= 10;
+		n += *s - '0';
+	}
+	*ok = s != *ps;
+	*ps = s;
+	return n;
+}
+
+static int
+lookup(char **s, char **tab, int len, int *ok)
+{
+	int nc, i;
+
+	*ok = 0;
+	for(i = 0; *tab; tab++){
+		nc = (len != -1) ? len : strlen(*tab);
+		if(cistrncmp(*s, *tab, nc) == 0){
+			*s += nc;
+			*ok = 1;
+			return i;
+		}
+		i++;
+	}
+	*ok = 0;
+	return -1;
+}
+
+Tm*
+tmparse(Tm *tm, char *fmt, char *str, Tzone *tz, char **ep)
+{
+	int depth, n, w, c0, zs, z0, z1, md, ampm, zoned, sloppy, tzo, ok;
+	vlong abs;
+	char *s, *p, *q;
+	Tzone *zparsed;
+	Tzabbrev *a;
+	Tzoffpair *m;
+
+	p = fmt;
+	s = str;
+	tzo = 0;
+	ampm = -1;
+	zoned = 0;
+	zparsed = nil;
+	sloppy = 0;
+	/* Default all fields */
+	tmtime(tm, 0, nil);
+	if(*p == '~'){
+		sloppy = 1;
+		p++;
+	}
+	while(*p){
+		w = 1;
+		c0 = *p++;
+		if(c0 == '?'){
+			w = -1;
+			c0 = *p++;
+		}
+		while(*p == c0){
+			if(w != -1) w++;
+			p++;
+		}
+		ok = 1;
+		switch(c0){
+		case 'Y':
+			switch(w){
+			case -1:
+				tm->year = getnum(&s, 4, &ok);
+				if(tm->year > 100) tm->year -= 1900;
+				break;
+			case 1:	tm->year = getnum(&s, 4, &ok) - 1900;	break;
+			case 2: tm->year = getnum(&s, 2, &ok);		break;
+			case 3:
+			case 4:	tm->year = getnum(&s, 4, &ok) - 1900;	break;
+			default: goto badfmt;
+			}
+			break;
+		case 'M':
+			switch(w){
+			case -1:
+				tm->mon = getnum(&s, 2, &ok) - 1;
+				if(!ok) tm->mon = lookup(&s, month, -1, &ok);
+				if(!ok) tm->mon = lookup(&s, month, 3, &ok);
+				break;
+			case 1:
+			case 2: tm->mon = getnum(&s, 2, &ok) - 1;	break;
+			case 3:	tm->mon = lookup(&s, month, 3, &ok);	break;
+			case 4:	tm->mon = lookup(&s, month, -1, &ok);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 'D':
+			switch(w){
+			case -1:
+			case 1:
+			case 2: tm->mday = getnum(&s, 2, &ok);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 'W':
+			switch(w){
+			case -1:
+				tm->wday = lookup(&s, wday, -1, &ok);
+				if(!ok) tm->wday = lookup(&s, wday, 3, &ok);
+				if(!ok) tm->wday = getnum(&s, 1, &ok) - 1;
+				break;
+			case 1: tm->wday = getnum(&s, 1, &ok) - 1;	break;
+			case 2:	tm->wday = lookup(&s, wday, 3, &ok);	break;
+			case 3:	tm->wday = lookup(&s, wday, -1, &ok);	break;
+			default: goto badfmt;
+			}
+			break;
+		case 'h':
+			switch(w){
+			case -1:
+			case 1:
+			case 2: tm->hour = getnum(&s, 2, &ok);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 'm':
+			switch(w){
+			case -1:
+			case 1:
+			case 2: tm->min = getnum(&s, 2, &ok);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 's':
+			switch(w){
+			case -1:
+			case 1:
+			case 2: tm->sec = getnum(&s, 2, &ok);		break;
+			default: goto badfmt;
+			}
+			break;
+		case 't':
+			switch(w){
+			case -1:
+			case 1:
+			case 2:
+			case 3:	tm->nsec += getnum(&s, 3, &ok)*1000000;	break;
+			}
+			break;
+		case 'u':
+			switch(w){
+			case -1:
+			case 1:
+			case 2:	tm->nsec += getnum(&s, 3, &ok)*1000;	break;
+			case 3:
+			case 4: tm->nsec += getnum(&s, 6, &ok)*1000;	break;
+			}
+			break;
+		case 'n':
+			switch(w){
+			case 1:
+			case 2:	tm->nsec += getnum(&s, 3, &ok);		break;
+			case 3:
+			case 4: tm->nsec += getnum(&s, 6, &ok);		break;
+			case -1:
+			case 5:
+			case 6: tm->nsec += getnum(&s, 9, &ok);		break;
+			}
+			break;
+		case 'z':
+			if(w != 1)
+				goto badfmt;
+		case 'Z':
+			zs = 0;
+			zoned = 1;
+			switch(*s++){
+			case '+': zs = 1; break;
+			case '-': zs = -1; break;
+			default: s--; break;
+			}
+			q = s;
+			switch(w){
+			case -1:
+			case 3:
+				for(a = tzabbrev; a->abbr; a++){
+					n = strlen(a->abbr);
+					if(cistrncmp(s, a->abbr, n) == 0 && !isalpha(s[n]))
+						break;
+				}
+				if(a->abbr != nil){
+					s += strlen(a->abbr);
+					zparsed = tzload(a->name);
+					if(zparsed == nil){
+						werrstr("unloadable zone %s (%s)", a->abbr, a->name);
+						if(w != -1)
+							return nil;
+					}
+					goto Zoneparsed;
+				}
+				for(m = milabbrev; m->abbr != nil; m++){
+					n = strlen(m->abbr);
+					if(cistrncmp(s, m->abbr, n) == 0 && !isalpha(s[n]))
+						break;
+				}
+				if(m->abbr != nil){
+					snprint(tm->zone, sizeof(tm->zone), "%s", m->abbr);
+					tzo = m->off;
+					goto Zoneparsed;
+				}
+				if(w != -1)
+					break;
+				/* fall through */
+			case 1:
+				/* offset: [+-]hhmm */
+				z0 = getnum(&s, 4, &ok);
+				if(s - q == 4){
+					z1 = z0 % 100;
+					if(z0/100 > 13 || z1 >= 60)
+						goto baddate;
+					tzo = zs*(3600*(z0/100) + 60*z1);
+					snprint(tm->zone, sizeof(tm->zone), "%c%02d%02d", zs<0?'-':'+', z0/100, z1);
+					goto Zoneparsed;
+				}
+				if(w != -1)
+					goto baddate;
+				/* fall through */
+			case 2:
+				s = q;
+				/* offset: [+-]hh:mm */
+				z0 = getnum(&s, 2, &ok);
+				if(*s++ != ':')
+					break;
+				z1 = getnum(&s, 2, &ok);
+				if(z1 > 60)
+					break;
+				tzo = zs*(3600*z0 + 60*z1);
+				snprint(tm->zone, sizeof(tm->zone), "%c%d02:%02d", zs<0?'-':'+', z0, z1);
+				goto Zoneparsed;
+			}
+			if(w != -1)
+				goto baddate;
+			/*
+			 * Final fuzzy fallback: If we have what looks like an
+			 * unknown timezone abbreviation, keep the zone name,
+			 * but give it a timezone offset of 0. This allows us
+			 * to avoid rejecting zones outside of RFC5322.
+			 */
+			for(s = q; *s; s++)
+				if(!isalpha(*s))
+					break;
+			if(s - q >= 3 && !isalpha(*s)){
+				strncpy(tm->zone, q, s - q);
+				tzo = 0;
+				ok = 1;
+				goto Zoneparsed;
+			}
+			goto baddate;
+Zoneparsed:
+			break;
+		case 'A':
+		case 'a':
+			if(cistrncmp(s, "am", 2) == 0)
+				ampm = 0;
+			else if(cistrncmp(s, "pm", 2) == 0)
+				ampm = 1;
+			else
+				goto baddate;
+			s += 2;
+			break;
+		case '[':
+			depth = 1;
+			while(*p){
+				if(*p == '[')
+					depth++;
+				if(*p == ']')
+					depth--;
+				if(*p == '\\')
+					p++;
+				if(depth == 0)
+					break;
+				if(*s == 0)
+					goto baddate;
+				if(*s++ != *p++)
+					goto baddate;
+			}
+			if(*p != ']')
+				goto badfmt;
+			p++;
+			break;
+		case '_':
+		case ',':
+		case ' ':
+
+			if(*s != ' ' && *s != '\t' && *s != ',' && *s != '\n' && *s != '\0')
+				goto baddate;
+			p += strspn(p, " ,_\t\n");
+			s += strspn(s, " ,\t\n");
+			break;
+		default:
+			if(*s == 0)
+				goto baddate;
+			if(*s++ != c0)
+				goto baddate;
+			break;
+		}
+		if(!ok)
+			goto baddate;
+	}
+	if(*p != '\0')
+		goto baddate;
+	if(ep != nil)
+		*ep = s;
+	if(!sloppy && ampm != -1 && (tm->hour < 1 || tm->hour > 12))
+		goto baddate;
+	if(ampm == 0 && tm->hour == 12)
+		tm->hour = 0;
+	else if(ampm == 1 && tm->hour < 12)
+		tm->hour += 12;
+	/*
+	 * If we're allowing sloppy date ranges,
+	 * we'll normalize out of range values.
+	 */
+	if(!sloppy){
+		if(tm->yday < 0 || tm->yday > 365 + isleap(tm->year + 1900))
+			goto baddate;
+		if(tm->wday < 0 || tm->wday > 6)
+			goto baddate;
+		if(tm->mon < 0 || tm->mon > 11)
+			goto baddate;
+		md = mdays[tm->mon];
+		if(tm->mon == 1 && isleap(tm->year + 1900))
+			md++;
+		if(tm->mday < 0 || tm->mday > md)
+			goto baddate;
+		if(tm->hour < 0 || tm->hour > 24)
+			goto baddate;
+		if(tm->min < 0 || tm->min > 59)
+			goto baddate;
+		if(tm->sec < 0 || tm->sec > 60)
+			goto baddate;
+		if(tm->nsec < 0 || tm->nsec > Nsec)
+			goto baddate;
+		if(tm->wday < 0 || tm->wday > 6)
+			goto baddate;
+	}
+
+	/*
+	 * Normalizing gives us the local time,
+	 * but because we havnen't applied the
+	 * timezone, we think we're GMT. So, we
+	 * need to shift backwards. Then, we move
+	 * the "GMT that was local" back to local
+	 * time.
+	 */
+	abs = tmnorm(tm);
+	tm->tzoff = tzo;
+	if(!zoned)
+		tzoffset(tz, abs, tm);
+	else if(zparsed != nil){
+		tzoffset(zparsed, abs, tm);
+		tzoffset(zparsed, abs + tm->tzoff, tm);
+	}
+	abs -= tm->tzoff;
+	if(tz != nil || !zoned)
+		tmtimens(tm, abs, tm->nsec, tz);
+	return tm;
+baddate:
+	werrstr("invalid date %s", str);
+	return nil;
+badfmt:
+	werrstr("garbled format %s near '%s'", fmt, p);
+	return nil;			
+}
+
+Tmfmt
+tmfmt(Tm *d, char *fmt)
+{
+	return (Tmfmt){fmt, d};
+}
+
+void
+tmfmtinstall(void)
+{
+	fmtinstall(L'τ', τconv);
+}
+
+/* These legacy functions need access to τconv */
+static char*
+dotmfmt(Fmt *f, ...)
+{
+	static char buf[30];
+	va_list ap;
+
+	va_start(ap, f);
+	f->runes = 0;
+	f->start = buf;
+	f->to = buf;
+	f->stop = buf + sizeof(buf) - 1;
+	f->flush = nil;
+	f->farg = nil;
+	f->nfmt = 0;
+	f->args = ap;
+	τconv(f);
+	va_end(ap);
+	buf[sizeof(buf) - 1] = 0;
+	return buf;
+}
+
+char*
+asctime(Tm* tm)
+{
+	Tmfmt tf;
+	Fmt f;
+
+	tf = tmfmt(tm, "WW MMM _D hh:mm:ss ZZZ YYYY\n");
+	return dotmfmt(&f, tf);
+}
+
diff -r c5110aa667d8 sys/src/libc/port/mkfile
--- a/sys/src/libc/port/mkfile	Wed Jul 29 13:56:03 2020 +0930
+++ b/sys/src/libc/port/mkfile	Fri Jul 31 08:22:43 2020 -0700
@@ -20,6 +20,7 @@
 	cleanname.c\
 	crypt.c\
 	ctype.c\
+	date.c\
 	encodefmt.c\
 	execl.c\
 	exits.c\

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

* Re: [9front] smtp/imap date bug, messages from the future
  2020-07-31 15:27 ` ori
@ 2020-08-08 11:25   ` sirjofri+ml-9front
  0 siblings, 0 replies; 4+ messages in thread
From: sirjofri+ml-9front @ 2020-08-08 11:25 UTC (permalink / raw)
  To: 9front

Hello,

> Can you try applying the rewrite of our date handling code
> and see if maybe I've accidentally fixed the issue?

Sadly, this didn't change anything.

> can you echo 'debug' into /mail/fs/ctl

echo: write error: unknown control message

sirjofri



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

end of thread, other threads:[~2020-08-08 11:25 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-07-31  7:59 smtp/imap date bug, messages from the future sirjofri+ml-9front
2020-07-31  8:02 ` [9front] " sirjofri+ml-9front
2020-07-31 15:27 ` ori
2020-08-08 11:25   ` sirjofri+ml-9front

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 NNTP newsgroup(s).