diff --git a/htmltest/check-link.go b/htmltest/check-link.go index 89174ed..bf1cf3a 100644 --- a/htmltest/check-link.go +++ b/htmltest/check-link.go @@ -5,6 +5,7 @@ import ( "github.com/wjdp/htmltest/issues" "github.com/wjdp/htmltest/output" "golang.org/x/net/html" + "net" "net/http" "net/url" "os" @@ -349,6 +350,71 @@ func (hT *HTMLTest) checkMailto(ref *htmldoc.Reference) { }) return } + + // split off domain, check mx, fallback to A or AAAA if that fails + var dnserr *net.DNSError + var ok bool + + domain := strings.Split(ref.URL.Opaque, "@")[1] + + // loop over the current domain until we have a valid result or have exhausted all possibilities + for domain != "" { + // if a simple MX lookup works, we are done, continue + if _, err := net.LookupMX(domain); err == nil { + break // success, time to exit + } else if dnserr, ok = err.(*net.DNSError); !ok || dnserr.Err != "no such host" { + // this isn't an error we are expecting to see here + hT.issueStore.AddIssue(issues.Issue{ + Level: issues.LevelWarning, + Message: "unable to perform LookupMX, unknown error", + Reference: ref, + }) + return + } + + // do we have to restart because of a CNAME + if cname, err := net.LookupCNAME(domain); err == nil && cname != domain { + // we have a valid CNAME, try with that. Loops return NXDOMAIN by default + domain = cname + continue + + } else if dnserr, ok = err.(*net.DNSError); !ok || dnserr.Err != "no such host" { + // this isn't an error we are expecting to see here + hT.issueStore.AddIssue(issues.Issue{ + Level: issues.LevelWarning, + Message: "unable to perform LookupCNAME, unknown error", + Reference: ref, + }) + return + } + + // an A or AAAA record here would be valid + if _, err := net.LookupHost(domain); err == nil { + break // its not ideal, but a valid A/AAAA record is acceptable for email + } else { + dnserr, ok = err.(*net.DNSError) + if !ok || dnserr.Err != "no such host" { + // we shouldn't see this here + hT.issueStore.AddIssue(issues.Issue{ + Level: issues.LevelWarning, + Message: "unable to perform LookupHost, unknown error", + Reference: ref, + }) + return + } + + if dnserr.Err == "no such host" { + // represents NXDOMAIN or no records + hT.issueStore.AddIssue(issues.Issue{ + Level: issues.LevelError, + Message: "email domain could not be resolved correctly", + Reference: ref, + }) + return + } + } + + } } func (hT *HTMLTest) checkTel(ref *htmldoc.Reference) { diff --git a/htmltest/check-link_test.go b/htmltest/check-link_test.go index 6d77659..9d74ba6 100644 --- a/htmltest/check-link_test.go +++ b/htmltest/check-link_test.go @@ -383,13 +383,44 @@ func TestMailtoBlank(t *testing.T) { tExpectIssue(t, hT, "mailto is empty", 1) } -func TestMailtoInvalid(t *testing.T) { +func TestMailtoInvalidFormat(t *testing.T) { // fails for invalid mailto links hT := tTestFile("fixtures/links/invalid_mailto_link.html") tExpectIssueCount(t, hT, 1) tExpectIssue(t, hT, "contains an invalid email address", 1) } +func TestMailtoInvalidCname(t *testing.T) { + // rubdomain with just a CNAME record without MX pointers + // email is still deliverable so should be accepted + // the records have been carefully created to ensure this is valid and will later fall under one of the other + // pass conditions + hT := tTestFile("fixtures/links/invalid_mailto_cname.html") + tExpectIssueCount(t, hT, 0) +} + +func TestMailtoInvalidCnameLoop(t *testing.T) { + // subdomain with just a CNAME record without MX records in a loop + // should be detected and fail as we can never resolve this + hT := tTestFile("fixtures/links/invalid_mailto_cname_loop.html") + tExpectIssueCount(t, hT, 1) + tExpectIssue(t, hT, "email domain could not be resolved correctly", 1) +} + +func TestMailtoInvalidNoRecords(t *testing.T) { + // domain with no records associated at all, must fail as completely unroutable + hT := tTestFile("fixtures/links/invalid_mailto_norecords.html") + tExpectIssueCount(t, hT, 1) + tExpectIssue(t, hT, "email domain could not be resolved correctly", 1) +} + +func TestMailtoInvalidAFallback(t *testing.T) { + // domain without MX records but with a valid A record + // email can be routed to the A record so should be accepted + hT := tTestFile("fixtures/links/invalid_mailto_fallback.html") + tExpectIssueCount(t, hT, 0) +} + func TestMailtoIgnore(t *testing.T) { // ignores mailto links when told to hT := tTestFileOpts("fixtures/links/blank_mailto_link.html", diff --git a/htmltest/fixtures/links/invalid_mailto_cname.html b/htmltest/fixtures/links/invalid_mailto_cname.html new file mode 100644 index 0000000..d66fed0 --- /dev/null +++ b/htmltest/fixtures/links/invalid_mailto_cname.html @@ -0,0 +1,10 @@ + + +
+ + +Meow me + + + + diff --git a/htmltest/fixtures/links/invalid_mailto_cname_loop.html b/htmltest/fixtures/links/invalid_mailto_cname_loop.html new file mode 100644 index 0000000..f8972ab --- /dev/null +++ b/htmltest/fixtures/links/invalid_mailto_cname_loop.html @@ -0,0 +1,10 @@ + + + + + +Meow me + + + + diff --git a/htmltest/fixtures/links/invalid_mailto_fallback.html b/htmltest/fixtures/links/invalid_mailto_fallback.html new file mode 100644 index 0000000..9c263a9 --- /dev/null +++ b/htmltest/fixtures/links/invalid_mailto_fallback.html @@ -0,0 +1,9 @@ + + + + +Meow me + + + + diff --git a/htmltest/fixtures/links/invalid_mailto_norecords.html b/htmltest/fixtures/links/invalid_mailto_norecords.html new file mode 100644 index 0000000..5510132 --- /dev/null +++ b/htmltest/fixtures/links/invalid_mailto_norecords.html @@ -0,0 +1,9 @@ + + + + +Meow me + + + +