Funneling messages from the server into the same field (ex: LOGIN-DELAY) as explicit decisions (ex: pop-server-not-great) seems risky; for example, an error message that includes "security" in it could cause us to claim there are sercurity problems when there aren't.

normalizeError for IMAP was mainly an evolved hack that grew out of not wanting to change imap.js too much and feature creep (some of which was implemented by a green contributor to e-mail).  (AKA no need to imitate other than unit tests being happy).

I would suggest that you separate the state machine bits of inferring an error based on what generated the error out.  Have Pop3Client return simple { name, message } errors.  Keep normalizeError around and have it do the reachable/retry/reportProblem stuff.  There will be some redundant re-processing for those annotations.  Or if you want to keep things fused, that's okay, but really separate the path of the things the server told us from the things we determined/inferred/etc.