diff --git a/dmarc/report.go b/dmarc/report.go
index d69a875..d844cda 100644
--- a/dmarc/report.go
+++ b/dmarc/report.go
@@ -3,12 +3,44 @@ package dmarc
import (
"encoding/xml"
"io"
+ "fmt"
+ "time"
+ "strconv"
+ "strings"
+ "net"
)
+const AuthResultType_DKIM = "dkim"
+const AuthResultType_SPF = "spf"
+
+
type DateRange struct {
- // TODO: should be int but Y! trailing spaces
- Begin string `xml:"begin"`
- End string `xml:"end"`
+ Begin time.Time
+ End time.Time
+}
+
+// Unmarshal the DateRange element into time.Time objects.
+// Yahoo tends to put some extra whitespace behind its
+// timestamps, so we trim that first.
+func (dr *DateRange) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ rangeStr := struct {
+ Begin string `xml:"begin"`
+ End string `xml:"end"`
+ }{}
+ d.DecodeElement(&rangeStr, &start)
+
+ begin, err := strconv.Atoi(strings.TrimSpace(rangeStr.Begin))
+ if err != nil {
+ return err
+ }
+ end, err := strconv.Atoi(strings.TrimSpace(rangeStr.End))
+ if err != nil {
+ return err
+ }
+
+ dr.Begin = time.Unix(int64(begin), 0).UTC()
+ dr.End = time.Unix(int64(end), 0).UTC()
+ return nil
}
type ReportMetadata struct {
@@ -35,23 +67,58 @@ type PolicyEvaluated struct {
}
type Row struct {
- // TODO: Figure out how to cast this to an IP
- SourceIp string `xml:"source_ip"`
+ SourceIp net.IP
Count int `xml:"count"`
PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"`
}
+func (r *Row) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ rowAlias := struct {
+ SourceIp string `xml:"source_ip"`
+ Count int `xml:"count"`
+ PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"`
+ }{}
+ d.DecodeElement(&rowAlias, &start)
+
+ r.Count = rowAlias.Count
+ r.PolicyEvaluated = rowAlias.PolicyEvaluated
+ r.SourceIp = net.ParseIP(rowAlias.SourceIp)
+ if r.SourceIp == nil {
+ return fmt.Errorf("Could not parse source_ip")
+ }
+
+ return nil
+}
+
type Identifiers struct {
HeaderFrom string `xml:"header_from"`
}
type AuthResult struct {
- // FIXME: this could be either DKIM or SPF
- XMLName xml.Name
+ Type string // Either SPF or DKIM
Domain string `xml:"domain"`
Result string `xml:"result"`
}
+func (ar *AuthResult) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ res := struct {
+ XMLName xml.Name
+ Domain string `xml:"domain"`
+ Result string `xml:"result"`
+ }{}
+ d.DecodeElement(&res, &start)
+
+ ar.Domain = res.Domain
+ ar.Result = res.Result
+
+ if res.XMLName.Local != AuthResultType_DKIM && res.XMLName.Local != AuthResultType_SPF {
+ return fmt.Errorf("Unrecognized AuthResult type: %s", res.XMLName.Local)
+ }
+
+ ar.Type = res.XMLName.Local
+ return nil
+}
+
type AuthResults struct {
AuthResult []AuthResult `xml:",any"`
}
@@ -63,36 +130,39 @@ type Record struct {
}
type FeedbackReport struct {
- XMLName xml.Name `xml:"feedback"`
- ReportMetadata ReportMetadata `xml:"report_metadata"`
+ Metadata ReportMetadata `xml:"report_metadata"`
PolicyPublished PolicyPublished `xml:"policy_published"`
Record []Record `xml:"record"`
}
-func ParseReader(xmlFileReader io.Reader) FeedbackReport {
- var f FeedbackReport
+func ParseReader(xmlFileReader io.Reader) (*FeedbackReport, error) {
+ var f *FeedbackReport
decoder := xml.NewDecoder(xmlFileReader)
var inElement string
for {
- t, _ := decoder.Token()
+ t, err := decoder.Token()
+ if t == nil && err == io.EOF {
+ break;
+ }
if t == nil {
- break
+ return nil, err
}
switch se := t.(type) {
case xml.StartElement:
inElement = se.Name.Local
if inElement == "feedback" {
- decoder.DecodeElement(&f, &se)
- // xmlerr := decoder.DecodeElement(&f, &se)
- // if xmlerr != nil {
- // fmt.Printf("decode error: %v\n", xmlerr)
- // }
- // fmt.Printf("XMLName: %#v\n", f)
+ err := decoder.DecodeElement(&f, &se)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, fmt.Errorf("Unknown root element: %s", inElement)
}
default:
}
}
- return f
+
+ return f, nil
}
diff --git a/dmarc/report_test.go b/dmarc/report_test.go
new file mode 100644
index 0000000..42f59c7
--- /dev/null
+++ b/dmarc/report_test.go
@@ -0,0 +1,115 @@
+package dmarc
+
+import (
+ "testing"
+ "strings"
+ //"fmt"
+ "time"
+)
+
+var reportXML = `
+
+
+ Yahoo! Inc.
+ postmaster@dmarc.yahoo.com
+ 1405588706.570207
+
+ 1405468800
+ 1405555199
+
+
+
+ example.com
+ r
+ r
+ none
+ 100
+
+
+
+ 127.0.0.1
+ 2
+
+ none
+ fail
+ pass
+
+
+
+ example.com
+
+
+
+ example.com
+ neutral
+
+
+ example.com
+ pass
+
+
+
+
+
+ 127.0.0.2
+ 988
+
+ none
+ fail
+ fail
+
+
+
+ idfactor.example.com
+
+
+
+ idfactor.example.com
+ neutral
+
+
+ idfactor.example.com
+ none
+
+
+
+ `
+
+func TestInvalidDocument(t *testing.T) {
+ xml := ``
+ report, err := ParseReader(strings.NewReader(xml))
+
+ if err == nil {
+ t.Errorf("Expected error, but got none")
+ }
+
+ if report != nil {
+ t.Errorf("Report should have been but wasn't")
+ }
+}
+
+func TestUnmarshalling(t *testing.T) {
+ report, err := ParseReader(strings.NewReader(reportXML))
+
+ if err != nil {
+ t.Errorf("Got error: %s:", err.Error())
+ }
+
+ begin := time.Unix(1405468800, 0).UTC()
+ if report.Metadata.DateRange.Begin != begin {
+ t.Errorf("report.Metadata.DateRange.Begin did not match expected date")
+ }
+
+ end := time.Unix(1405555199, 0).UTC()
+ if report.Metadata.DateRange.End != end {
+ t.Errorf("report.Metadata.DateRange.end did not match expected date")
+ }
+
+ if report.PolicyPublished.Domain != "example.com" {
+ t.Errorf("report.PolicyPublished.Domain was '%s', expected 'example.com'", report.PolicyPublished.Domain)
+ }
+
+ if len(report.Record) != 2 {
+ t.Errorf("report.Record was expected to contain 2 items, only got: %d", len(report.Record))
+ }
+}
\ No newline at end of file
diff --git a/dmarcparser.go b/dmarcparser.go
index c1cb478..7ef2330 100644
--- a/dmarcparser.go
+++ b/dmarcparser.go
@@ -1,9 +1,9 @@
package dmarcaggparser
import (
+ "dmarc"
"flag"
"fmt"
- "dmarc"
"os"
)