Skip to content

Add basic checking of mx records for mailto links #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions htmltest/check-link.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 != "" {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment here would be nice: we're going to loop until we find a valid record or error

// 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact error on Linux for the loop CNAME test is server misbehaving (value of dnserr.Err) which seems in-line-ish with nslookup:

~ ❯ nslookup loop1.dtaylor.uk
Server:		127.0.1.1
Address:	127.0.1.1#53

** server can't find loop1.dtaylor.uk: SERVFAIL

Likely an error discrepancy between Linux and OSX. You may need to restructure a bit to handle this cleanly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is infact an issue, no such host is an isolated case where we want to proceed to checking A records. Should a CNAME loop not produce an warning of some variety?
I accept that this may be inconsistent between platforms, but I feel the Linux way is actually better in this case and the OS X way is an acceptable fallback?

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) {
Expand Down
33 changes: 32 additions & 1 deletion htmltest/check-link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guessing this isn't done yet. In general write the tests with expected result and add t.Skip("Not yet dealt with") at the top.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test was complete, it was checking that just a CNAME pointing to another domain that was valid would work as expected, comments should hopefully clear that up.

}

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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

}

func TestMailtoIgnore(t *testing.T) {
// ignores mailto links when told to
hT := tTestFileOpts("fixtures/links/blank_mailto_link.html",
Expand Down
10 changes: 10 additions & 0 deletions htmltest/fixtures/links/invalid_mailto_cname.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>

<body>

<!-- www.golang.org CNAME golang.org - seems appropriate -->
<a href="mailto:helloworld@cname_to_a_with_no_mx.dtaylor.uk">Meow me</a>

</body>

</html>
10 changes: 10 additions & 0 deletions htmltest/fixtures/links/invalid_mailto_cname_loop.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>

<body>

<!-- www.golang.org CNAME golang.org - seems appropriate -->
<a href="mailto:[email protected]">Meow me</a>

</body>

</html>
9 changes: 9 additions & 0 deletions htmltest/fixtures/links/invalid_mailto_fallback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>

<body>

<a href="mailto:[email protected]">Meow me</a>

</body>

</html>
9 changes: 9 additions & 0 deletions htmltest/fixtures/links/invalid_mailto_norecords.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>

<body>

<a href="mailto:[email protected]">Meow me</a>

</body>

</html>