mirror of
https://github.com/hanzoai/email-verifier.git
synced 2026-10-30 15:04:29 +00:00
Two fixes in parseSMTPError, both reached by the same question: where does the reply text come from, and which part of it may be read as a statement about the recipient. The keyword check that reads a reply as being about the address -- "user unknown", "does not exist" and eight more -- ran before the switch, and two things were wrong with that. It applied to transient replies. A 4xx says the server could not answer now, whatever its text mentions, so it cannot be read as a statement about the address; but any 4xx naming the recipient was answered ErrServerUnavailable, which CheckSMTP reads as evidence that the domain is not a catch-all. Now confined to 5xx, bounded at both ends: ParseSMTPError is exported, and a code outside SMTP's classes says nothing about a recipient either. Message moves with Details. For a reply code the switch does not name, parseBasicErr builds both from the same text, so a 501 reports 501 5.7.1 ... where it used to report 501 "5.7.1 ...". Tested, and the changelog says so. This is a defect by the RFC's definition of the class rather than an observed one: the only 4xx among the nineteen providers probed for #222 is Microsoft's 452 4.5.3 Recipients belong to multiple regions, which matches none of the phrases and already reached the switch. 5xx classification is unchanged. Two things that would have changed it were tried and dropped, because both turn on the keyword lists being precise and they are not. Letting a blocklist phrase outrank a recipient phrase reclassified "recipient address rejected: access denied" as blocked. That is Postfix's wording for an unknown recipient; it matched only because our list carries the very broad "denied". Widening the failure bound to >= 400 put 400 through keyword matching, which changed its answer too. The override of 551 through 554 by recipient phrases is likewise left alone: believing the code or the text there is a judgement call, not a defect. Details carried a rendering rather than a reply. textproto.Error formats with %q, which wraps the text in quotes the server never sent and escapes the newlines of a multi-line reply into a literal \n; Details is serialised, so every consumer saw that. replyText reads Code and Msg, the fields textproto parsed the reply into, and falls back to Error() only for errors that carry no reply of their own -- dial failures, DNS failures, and the flattened replies the exported ParseSMTPError also accepts. Those are unchanged. The blocklist keywords were two lists. The 550 branch knew "blacklisted" and "block list" and parseBasicErr did not, so whether a phrase counted depended on whether the reply code was one the switch happens to name: a 501 mentioning a blocklist was reported verbatim while a 550 was reported as blocked. One list now, and a test walks it against a code inside the switch and one outside. The test #222 added to pin the %q behaviour failed here, which is what it was for, and is replaced by tests for the behaviour that supersedes it. Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
7.7 KiB
7.7 KiB
Change log
Unreleased
- Internal: Lint the whole repository rather than only changed lines, clearing 42 pre-existing findings; enable
usestdlibvarsandintrange; fix six error assertions that only checked that an error was non-nil #217 - Fix:
Reachableisno, notunknown, when the mail server explicitly refuses the address andDisableCatchAllCheck()is in effect.SMTP.CatchAllwas set before the catch-all probe ran and only ever cleared by a 550-class refusal, so with the probe disabled it stayedtrueand suppressed the verdict the address check had already produced #220 - Feature:
LookupError.EnhancedCode()exposes the RFC 3463 enhanced status code a server sent, for example5.1.1. The class digit is dependable, the subject digit is not a verdict -- Yandex answers an unknown recipient with the same5.7.1that Apple uses to say our sending IP is blocklisted -- and it is empty for the providers that send no code at all #222 - Fix:
LookupError.Details, andMessagefor a reply code the classifier does not recognise, carry the SMTP reply as it arrived rather than Go's%qrendering of it, which added quotes and escaped the newlines of a multi-line reply. A transient 4xx is no longer read as a permanent statement about the recipient #223
v1.5.0
- Breaking: Supported Go versions are now the three most recent releases, and
go.modrequires the oldest of them, Go 1.25. The versions ofgolang.org/x/netandgolang.org/x/textthat fix GO-2026-5026 and GO-2026-5970 require it, and both were reachable fromVerify,CheckMXandCheckSMTPthroughidna.ToASCII-- the second of them an infinite loop on caller-supplied input #212 - Fix:
AddDisposableDomainsno longer races the refresh thatEnableAutoUpdateDisposable()schedules. Calling it from another goroutine while a refresh was running was afatal error: concurrent map iteration and map write, which brings the process down and norecovercan catch #211 - Fix: A small allowlist,
cmd/build_metadata/disposable_allowlist.txt, keeps real providers the upstream list misclassifies out of the disposable set -- currentlyhush.com,lavabit.com,mail2world.com,qmail.com,4x4man.comandalphafrau.de. It applies to the generated list and to everyEnableAutoUpdateDisposable()refresh, and since the free list is the free candidates minus the disposable one, these domains are now reported free rather than neither #209 - Breaking: The generated disposable list now comes from the same source
EnableAutoUpdateDisposable()fetches at runtime. The two had diverged, so enabling auto-update used to replace most of the baked-in list; it is now a no-op.IsDisposablegains 37679 domains and loses 96017, the vast majority of which no longer resolve #208 - Fix: The free-domain list is built from its original sources instead of an aggregate that had begun merging disposable blocklists into itself.
IsFreeDomainandSuggestDomainno longer treat throwaway domains as free providers, 108 carrier and portal mailboxes are recognised, andIsFreeDomain("atlanticbb.net")works -- the generated key carried a stray no-break space #207 - Fix:
IsRoleAccountrecognisescto,ctos,cfoandcfos. They were added to the source list in December 2025 but the generated map was never rebuilt, so they returnedfalseuntil now #205 - Feature: Support a custom DNS resolver for MX and SMTP host lookups via
Resolver()#191 - Fix:
Verifykeeps the SMTP result when the exchange fails partway.SMTPused to come back nil, which could not distinguish a host that never answered from one that answered and refused the sender #201 - Fix:
Reachableisno, notunknown, when the MX lookup fails withno such host, andVerifyreturnsErrNoSuchHost#139 - Fix:
Suggestionis filled in for disposable domains, which used to return early before the suggestion was computed #140 - Breaking: The free-domain sources drop six forks of one 2014 gist for the list they were forked from. One of the six was contributing throwaway domains rather than providers -- 77% of its 23,882 entries appear on disposable blocklists -- so
IsFreeDomainloses 2,009 domains it should not have had #169 - Breaking:
LookupErrorwraps the error it was derived from, reachable viaerrors.Is/errors.As.ParseSMTPErrorno longer returns a nil*LookupErrorfor a non-nil input. Adds an unexported field, so whole-struct comparison against aLookupErrorliteral no longer matches #202 - Breaking: Remove the non-functional Yahoo API verifier;
EnableAPIVerifier(YAHOO)and theYAHOOconstant are gone #198 - Fix: Yahoo API test panic no longer aborts the test suite #196
v1.4.1
- Feature: Configurable timeouts via
ConnectTimeout()andOperationTimeout()#110 - Breaking: Remove the Gmail API verifier;
EnableAPIVerifier(GMAIL)and theGMAILconstant are gone #113 - Update the free domain sources and rebuild the generated metadata #127
- Update Dependencies
v1.4.0
- Feature: Support Gmail&Yahoo SMTP check by API #88
- Optimization: Return HasMXRecord as true when at least one valid mx records exist #94
- Update Dependencies
v1.3.3
- Making catchAll detection optional #76
- When the user enables
EnableAutoUpdateDisposable(), the disposable domains configuration is updated once by default. - Update test cases
- Update Dependencies
v1.3.2
- Uses x/net/proxy to fix issue when using SOCKS5
v1.3.1
- Fix a bug:
IsDisposable()matches the complete email domain - Update dependent metadata
- Update Dependencies
v1.3.0
- Support setting SOCKS5 proxy to perform
CheckSMTP() - Make pkg compatible with earlier versions of Go
v1.2.0
- Support adding custom disposable email domains
- Fix a wrong reference in README
- Update dependent metadata
- Update Dependencies
v1.1.0
- Performance optimization:
- reduce Result struct size from 96 to 80
ParseAddress()returnSyntaxinstead of reference, for reducing GC pressure and improve memory locality.
- Provide a simple API server
- Bugfix: gravatar images may not exist
v1.0.3
- Add a New feature: domain suggestion (typo check)
v1.0.2
- Add build metadata tools to generate metadata_*.go files
- Update load meta data logic