From 0925197f7f2f420570e58d670cb50b092c26c56a Mon Sep 17 00:00:00 2001 From: Tomas Korbar Date: Fri, 14 Aug 2026 13:36:41 +0200 Subject: [PATCH] Fix UTC offset calculation in Date headers Resolves: RHEL-240793 --- s-nail-utc-offset.patch | 375 ++++++++++++++++++++++++++++++++++++++++ s-nail.spec | 8 +- 2 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 s-nail-utc-offset.patch diff --git a/s-nail-utc-offset.patch b/s-nail-utc-offset.patch new file mode 100644 index 0000000..b389794 --- /dev/null +++ b/s-nail-utc-offset.patch @@ -0,0 +1,375 @@ +From 60ed19eed59a13f494bf0880e132208a9f4c6bc1 Mon Sep 17 00:00:00 2001 +From: Steffen Nurpmeso +Date: Sun, 7 Nov 2021 02:43:37 +0100 +Subject: [PATCH] FIX UTC offset "calculation" (Andrea Biardi).. + +Andrea (thanks!) reported to the ML: + + Looking at the source of s-nail, I tracked it all down to a + naive/incorrect handling of tm->tm_isdst in mkdate() in sendout.c (the + same logic appears in other places, for example see the comment in + src/mx/header.c that reads "/* TODO simply adding an hour for ISDST + is .. buuh */".) + +Yeah the issue was known, but i hoped for SU tools.. whatever. + + After a lot of research (and I feel I've become an expert on the + subject by now!) this is what I've found: + + In 2018, the tzdata maintainers (IANA) corrected a historical mistake + with the Europe/Dublin timezone. The mistake was rooted in a + misunderstanding of whether IST meant "Irish Summer Time" or "Irish + Standard Time". + + The problem was discussed at great length + (http://mm.icann.org/pipermail/tz/2018-January/thread.html) and it was + concluded that IST really meant Irish *Standard* Time (in constrast + with, say, British *Summer* Time), and that this standard time is + defined as UTC+0100. + + This corresponds to the article at + https://en.wikipedia.org/wiki/Time_in_the_Republic_of_Ireland and the + notes at https://en.wikipedia.org/wiki/Winter_time_(clock_lag); the + source archive of tzdata has a long section dedicated to this problem + and a large set of official references and links to + www.irishstatutebook.ie. + + Once the question was settled, the only possible solution for keeping + the Irish local time in sync with the rest of the world (for example, + Belfast & London) was for IANA to _reverse_ the functioning of the DST + flag for Ireland. The result is that in the current IANA timezone + database (2021e), Europe/Dublin has DST applied in *winter*, with an + adjustment of -1h (that is, negative). + + Digging deeper, one uncovers that there are a few other countries that + have (or once had) the same time-switch mechanism as Ireland; + amongst others, https://github.com/MenoData/Time4J/issues/742 also + concedes that negative DST is a reality. + + In s-nail, the logic that works out the UTC offset does the right + thing in my testcase (october 2021, Ireland = UTC+0100), but then upon + inspecting tm->tm_isdst it sees that DST is in effect (remember, + flag has been reversed, so DST in Ireland is on in winter time) it adds + one hour (it should subtract one, because the adjustment is negative). + + That's why I get a +0200 instead of +0000 out of s-nail. + + You may wonder why this problem hasn't been noticed by Irish people in + the past three years (hey, there's quite an IT industry over here!). + + It turns out that the introduction of a negative DST adjustment caused + all sorts of bugs back in 2018; in the source distribution of IANA's + tzdata, one can spot this inside ./europe: + + # In January 2018 we discovered that the negative SAVE values in the + # Eire rules cause problems with tests for ICU [...] and with tests + # for OpenJDK [...] + # To work around this problem, the build procedure can translate the + # following data into two forms, one with negative SAVE values and the + # other form with a traditional approximation for Irish timestamps + # after 1971-10-31 02:00 UTC; although this approximation has tm_isdst + # flags that are reversed, its UTC offsets are correct and this often + # suffices. This source file currently uses only nonnegative SAVE + # values, but this is intended to change and downstream code should + # not rely on it. + + So, a temporary hack was put in place in order to allow distro + maintainers to retain the old broken convention of IST and support + buggy software, but it is clear that the current (and technically, and + politically, correct) implementation of a negative DST adjustment for + Ireland is there to stay. + + As a matter of fact, the distro maintainer can choose to compile + tzdata to keep buggy software happy ("make DATAFORM=rearguard"), + which replicates the behaviour of tzdata prior to 2018. Many distros + seem to be doing that for one reason or another, while some have passed + the upstream change down to their users (probably, without knowing). + + (also, remember that the IANA tz database is basically the only source + of tz data and is used globally by pretty much anybody who has a system + clock; I can see why embedded platforms may not be easy to patch to + support the negative DST adjustment). + + This explains why I see this problem in Slackware, but not on, for + example, CentOS (and also why some tools work correctly, and some, like + s-nail in this case, does not). + + I am in the process of evaluating different approaches to solve this + bug in a portable way (e.g. without relying on non-standard extensions + like tm->tm_gmtoff, strftime's "%z" or timelocal()/gmtime()). + +He then also mentioned + +tm_gmtoff seems like the easiest way. If you're interested in +portability, the only *portable* way of finding the offset from UTC +that I've found is: + + struct tm l,g; + g = *gmtime(&t); + l = *localtime(&t); + + and work out the difference between g and l by checking the members + (e.g. normalizing to either number of minutes) with something like: + + diff = (l->tm_sec-g->tm_sec)/60 + +(l->tm_min-g->tm_min) + +(l->tm_hour-g->tm_hour)*60 + /* + compensate for day/month/year difference */ + +And that is an interesting approach that in one way or another +other MUAs have gone. Do so, too! + +Many thanks for the effort, Andrea. +--- + include/mx/nailfuns.h | 5 +++++ + mk/make-config.sh | 10 +++++++++ + src/mx/auxlily.c | 47 +++++++++++++++++++++++++++++++++++++++++++ + src/mx/header.c | 16 +++++---------- + src/mx/imap-search.c | 19 +++++++---------- + src/mx/obs-imap.c | 18 ++++++----------- + src/mx/sendout.c | 14 ++++++------- + 7 files changed, 87 insertions(+), 42 deletions(-) + +diff --git a/include/mx/nailfuns.h b/include/mx/nailfuns.h +index 9c1f2a58..f912944b 100644 +--- a/include/mx/nailfuns.h ++++ b/include/mx/nailfuns.h +@@ -294,6 +294,11 @@ FL struct n_timespec const *n_time_now(boole force_update); + FL void time_current_update(struct time_current *tc, + boole full_update); + ++/* TZ difference in seconds. ++ * secsepoch is only used if any of the tm's is NIL. */ ++FL s32 n_time_tzdiff(s64 secsepoch, struct tm const *utcp_or_nil, ++ struct tm const *localp_or_nil); ++ + /* ctime(3), but do ensure 26 byte limit, do not crash XXX static buffer. + * NOTE: no trailing newline */ + FL char *n_time_ctime(s64 secsepoch, struct tm const *localtime_or_nil); +diff --git a/mk/make-config.sh b/mk/make-config.sh +index 434a9039..ea822428 100644 +--- a/mk/make-config.sh ++++ b/mk/make-config.sh +@@ -2104,6 +2104,16 @@ int main(void){ + fi + fi + ++link_check tm_gmtoff 'struct tm::tm_gmtoff' '#define mx_HAVE_TM_GMTOFF' << \! ++#include ++int main(void){ ++ time_t t; ++ ++ t = time((void*)0); ++ return gmtime(&t)->tm_gmtoff != 0; ++} ++! ++ + ## + ## optional and selectable + ## +diff --git a/src/mx/auxlily.c b/src/mx/auxlily.c +index 54b89d50..bf386815 100644 +--- a/src/mx/auxlily.c ++++ b/src/mx/auxlily.c +@@ -743,6 +743,53 @@ jredo: + NYD_OU; + } + ++FL s32 ++n_time_tzdiff(s64 secsepoch, struct tm const *utcp_or_nil, ++ struct tm const *localp_or_nil){ ++ struct tm tmbuf[2], *tmx; ++ time_t t; ++ s32 rv; ++ NYD2_IN; ++ UNUSED(utcp_or_nil); ++ ++ rv = 0; ++ ++ if(localp_or_nil == NIL){ ++ t = S(time_t,secsepoch); ++ if((tmx = localtime(&t)) == NIL) ++ goto jleave; ++ tmbuf[0] = *tmx; ++ localp_or_nil = &tmbuf[0]; ++ } ++ ++#ifdef mx_HAVE_TM_GMTOFF ++ rv = localp_or_nil->tm_gmtoff; ++ ++#else ++ if(utcp_or_nil == NIL){ ++ t = S(time_t,secsepoch); ++ if((tmx = gmtime(&t)) == NIL) ++ goto jleave; ++ tmbuf[1] = *tmx; ++ utcp_or_nil = &tmbuf[1]; ++ } ++ ++ rv = ((((localp_or_nil->tm_hour - utcp_or_nil->tm_hour) * 60) + ++ (localp_or_nil->tm_min - utcp_or_nil->tm_min)) * 60) + ++ (localp_or_nil->tm_sec - utcp_or_nil->tm_sec); ++ ++ if((t = (localp_or_nil->tm_yday - utcp_or_nil->tm_yday)) != 0){ ++ s32 const ds = 24 * 60 * 60; ++ ++ rv += (t == 1) ? ds : -S(s32,ds); ++ } ++#endif ++ ++jleave: ++ NYD2_OU; ++ return rv; ++} ++ + FL char * + n_time_ctime(s64 secsepoch, struct tm const *localtime_or_nil){/* TODO err*/ + /* Problem is that secsepoch may be invalid for representation of ctime(3), +diff --git a/src/mx/header.c b/src/mx/header.c +index b2d727dc..91947107 100644 +--- a/src/mx/header.c ++++ b/src/mx/header.c +@@ -2748,9 +2748,8 @@ FL time_t + unixtime(char const *fromline) + { + char const *fp, *xp; +- time_t t, t2; +- s32 i, year, month, day, hour, minute, second, tzdiff; +- struct tm *tmptr; ++ time_t t; ++ s32 i, year, month, day, hour, minute, second; + NYD2_IN; + + for (fp = fromline; *fp != '\0' && *fp != '\n'; ++fp) +@@ -2786,14 +2785,9 @@ unixtime(char const *fromline) + goto jinvalid; + if ((t = combinetime(year, month, day, hour, minute, second)) == (time_t)-1) + goto jinvalid; +- if((t2 = mktime(gmtime(&t))) == (time_t)-1) +- goto jinvalid; +- tzdiff = t - t2; +- if((tmptr = localtime(&t)) == NULL) +- goto jinvalid; +- if (tmptr->tm_isdst > 0) +- tzdiff += 3600; /* TODO simply adding an hour for ISDST is .. buuh */ +- t -= tzdiff; ++ ++ t += n_time_tzdiff(t, NIL, NIL); ++ + jleave: + NYD2_OU; + return t; +diff --git a/src/mx/imap-search.c b/src/mx/imap-search.c +index 3b129255..458c6a29 100644 +--- a/src/mx/imap-search.c ++++ b/src/mx/imap-search.c +@@ -615,9 +615,8 @@ jleave: + static time_t + _imap_read_date(char const *cp) + { +- time_t t, t2; +- s32 year, month, day, i, tzdiff; +- struct tm *tmptr; ++ time_t t; ++ s32 year, month, day, i; + char const *xp, *yp; + NYD_IN; + +@@ -641,16 +640,12 @@ _imap_read_date(char const *cp) + goto jerr; + if (yp[0] != '\0' && (yp[1] != '"' || yp[2] != '\0')) + goto jerr; +- if ((t = combinetime(year, month, day, 0, 0, 0)) == (time_t)-1) ++ ++ if((t = combinetime(year, month, day, 0, 0, 0)) == (time_t)-1) + goto jleave/*jerr*/; +- if((t2 = mktime(gmtime(&t))) == (time_t)-1) +- goto jerr; +- tzdiff = t - t2; +- if((tmptr = localtime(&t)) == NULL) +- goto jerr; +- if (tmptr->tm_isdst > 0) +- tzdiff += 3600; +- t -= tzdiff; ++ ++ t += n_time_tzdiff(t, NIL, NIL); ++ + jleave: + NYD_OU; + return t; +diff --git a/src/mx/obs-imap.c b/src/mx/obs-imap.c +index 095a594a..230599f4 100644 +--- a/src/mx/obs-imap.c ++++ b/src/mx/obs-imap.c +@@ -4615,26 +4615,19 @@ imap_make_date_time(time_t t) + char const *mn; + s32 y, md, th, tm, ts; + struct tm *tmp; +- int tzdiff, tzdiff_hour, tzdiff_min; +- time_t t2; ++ int tzdiff_hour, tzdiff_min; + NYD2_IN; + + jredo: +- if((t2 = mktime(gmtime(&t))) == (time_t)-1){ +- t = 0; +- goto jredo; +- } +- tzdiff = t - t2; + if((tmp = localtime(&t)) == NULL){ + t = 0; + goto jredo; + } + +- tzdiff_hour = (int)(tzdiff / 60); +- tzdiff_min = tzdiff_hour % 60; +- tzdiff_hour /= 60; +- if (tmp->tm_isdst > 0) +- tzdiff_hour++; ++ tzdiff_min = S(int,n_time_tzdiff(t, NIL, tmp)); ++ tzdiff_min /= 60; /* TODO su_TIME_MIN_SECS */ ++ tzdiff_hour = tzdiff_min / 60; ++ tzdiff_min %= 60; /* TODO su_TIME_HOUR_MINS */ + + if(UNLIKELY((y = tmp->tm_year) < 0 || y >= 9999/*S32_MAX*/ - 1900)){ + y = 1970; +@@ -4659,6 +4652,7 @@ jredo: + + snprintf(s, sizeof s, "\"%02d-%s-%04d %02d:%02d:%02d %+03d%02d\"", + md, mn, y, th, tm, ts, tzdiff_hour, tzdiff_min); ++ + NYD2_OU; + return s; + } +diff --git a/src/mx/sendout.c b/src/mx/sendout.c +index 00dab059..0eb734d8 100644 +--- a/src/mx/sendout.c ++++ b/src/mx/sendout.c +@@ -2435,17 +2435,17 @@ FL int + mkdate(FILE *fo, char const *field) + { + struct tm tmpgm, *tmptr; +- int tzdiff, tzdiff_hour, tzdiff_min, rv; ++ int tzdiff_hour, tzdiff_min, rv; + NYD_IN; + + su_mem_copy(&tmpgm, &time_current.tc_gm, sizeof tmpgm); +- tzdiff = time_current.tc_time - mktime(&tmpgm); +- tzdiff_hour = (int)(tzdiff / 60); +- tzdiff_min = tzdiff_hour % 60; +- tzdiff_hour /= 60; + tmptr = &time_current.tc_local; +- if (tmptr->tm_isdst > 0) +- ++tzdiff_hour; ++ ++ tzdiff_min = S(int,n_time_tzdiff(time_current.tc_time, NIL, tmptr)); ++ tzdiff_min /= 60; /* TODO su_TIME_MIN_SECS */ ++ tzdiff_hour = tzdiff_min / 60; ++ tzdiff_min %= 60; /* TODO su_TIME_HOUR_MINS */ ++ + rv = fprintf(fo, "%s: %s, %02d %s %04d %02d:%02d:%02d %+05d\n", + field, + n_weekday_names[tmptr->tm_wday], +-- +2.54.0 + diff --git a/s-nail.spec b/s-nail.spec index dff6842..456d067 100644 --- a/s-nail.spec +++ b/s-nail.spec @@ -1,6 +1,6 @@ Name: s-nail Version: 14.9.22 -Release: 9%{?dist} +Release: 10%{?dist} Summary: Environment for sending and receiving mail # Everything is ISC except parts coming from the original Heirloom mailx which are BSD @@ -15,6 +15,8 @@ Source2: steffen.asc Patch0: s-nail-testsuite.patch # upstream commit https://github.com/sdaoden/s-mailx/commit/c3ef40c2b63ad7785571802c4100581ebf228a1f.patch Patch1: s-nail-bcc-in-subject.patch +# upstream commit https://github.com/sdaoden/s-mailx/commit/60ed19eed59a13f494bf0880e132208a9f4c6bc1.patch +Patch2: s-nail-utc-offset.patch BuildRequires: make BuildRequires: gnupg2 @@ -142,6 +144,10 @@ fi %changelog +* Fri Aug 14 2026 Tomas Korbar - 14.9.22-10 +- Fix UTC offset calculation in Date headers +- Resolves: RHEL-240793 + * Wed Oct 29 2025 Tomas Korbar - 14.9.22-9 - Stop adding BCC lines to subject - Resolves: RHEL-110823