Skip to content

Add fetchInternalDate; fix RFC 3501 zone rendering in date-time output - #124

Open
jappeace-sloth wants to merge 2 commits into
qnikst:masterfrom
jappeace-sloth:internaldate-support
Open

Add fetchInternalDate; fix RFC 3501 zone rendering in date-time output#124
jappeace-sloth wants to merge 2 commits into
qnikst:masterfrom
jappeace-sloth:internaldate-support

Conversation

@jappeace-sloth

@jappeace-sloth jappeace-sloth commented Aug 30, 2026

Copy link
Copy Markdown

Motivation

We migrate small webshops between hosting providers, and when a shop leaves its old platform the bundled mailbox is terminated with the subscription. To preserve the owner's mail we copy every message from the old account into the new provider's mailbox over IMAP — admittedly a slightly unusual use of an IMAP client library, but one we actually ran into. appendFull already lets us write a message with its original flags and an explicit date, but nothing in the library could read a message's INTERNALDATE, so the copied mail lost all its dates. We ended up hand-rolling a minimal client for this one gap; this PR upstreams the missing piece so we can use HaskellNet instead.

What's added

  • fetchInternalDate :: IMAPConnection -> UID -> IO (Either FetchInternalDateError CalendarTime) — reads a message's INTERNALDATE, built on fetchByByteString like the other fetchers. Failures (InternalDateAbsent, InternalDateInvalid) are returned, not thrown, so the caller decides what a missing or malformed date means.
  • stringToDatetimeIMAP :: String -> Either DatetimeParseError CalendarTime — the inverse of datetimeToStringIMAP; accepts the quoted wire form and space-padded single-digit days per the RFC 3501 grammar. DatetimeParseError has a constructor per failure mode, each carrying the offending fragment. Both functions and both error types are exported, so fetchInternalDate's result can be fed straight back into appendFull.

What's fixed

Adding the inverse exposed a bug in datetimeToStringIMAP's zone rendering: the offset-in-seconds was divided by 3600 and zero-padded to four digits, so UTC+1 was emitted as +0001 (i.e. one minute) and negative offsets produced garbage like -000-1. Any appendFull with a non-UTC CalendarTime sent a malformed or wrong date-time. The zone now renders as (+/-)HHMM per RFC 3501; show4 had no remaining users and is removed.

Tests

HUnit cases for rendering (including a negative half-hour zone), parsing, the render/parse round-trip, space-padded days, and rejection of malformed input, plus a scripted-connection test for fetchInternalDate in the style of the existing fetch API tests. All 64 cases pass (cabal test, GHC 9.10.3).

jappeace-sloth and others added 2 commits August 30, 2026 16:18
fetchInternalDate reads a message's INTERNALDATE as a CalendarTime,
via the new stringToDatetimeIMAP (the inverse of datetimeToStringIMAP;
accepts the quoted form and space-padded days). Motivation: appendFull
can already write a message with an explicit date, but nothing in the
library could read one, so copying mail from one account to another
(a provider migration) lost every message's date.

While adding the inverse, the round-trip exposed a bug in
datetimeToStringIMAP's zone rendering: the offset-in-seconds was
divided by 3600 and zero-padded to four digits, so +0100 was emitted
as +0001 and negative offsets produced garbage like -000-1. The zone
is now rendered as (+/-)HHMM per RFC 3501; show4 had no other users
and is removed.

Tests: HUnit cases for rendering (including a negative half-hour
zone), parsing, the round-trip, space-padded days and rejection of
malformed input, plus a scripted-connection test for
fetchInternalDate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stringToDatetimeIMAP now returns Either DatetimeParseError
CalendarTime, with a constructor per failure mode, each carrying the
offending fragment; fetchInternalDate returns
IO (Either FetchInternalDateError CalendarTime) instead of calling
fail, so the caller decides both the failure behaviour and the
wording. Tests assert exact error values for malformed dates, a
signless zone and an unknown month, plus the absent-INTERNALDATE
path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant