12	"github.com/mjl-/mox/message"
 
13	"github.com/mjl-/mox/mlog"
 
14	"github.com/mjl-/mox/moxio"
 
17var ErrNoReport = errors.New("no tlsrpt report found")
 
21// Report is a TLSRPT report, transmitted in JSON format.
 
23	OrganizationName string          `json:"organization-name"`
 
24	DateRange        TLSRPTDateRange `json:"date-range"`
 
25	ContactInfo      string          `json:"contact-info"` // Email address.
 
26	ReportID         string          `json:"report-id"`
 
27	Policies         []Result        `json:"policies"`
 
30// note: with TLSRPT prefix to prevent clash in sherpadoc types.
 
31type TLSRPTDateRange struct {
 
32	Start time.Time `json:"start-datetime"`
 
33	End   time.Time `json:"end-datetime"`
 
36// UnmarshalJSON is defined on the date range, not the individual time.Time fields
 
37// because it is easier to keep the unmodified time.Time fields stored in the
 
39func (dr *TLSRPTDateRange) UnmarshalJSON(buf []byte) error {
 
41		Start xtime `json:"start-datetime"`
 
42		End   xtime `json:"end-datetime"`
 
44	if err := json.Unmarshal(buf, &v); err != nil {
 
47	dr.Start = time.Time(v.Start)
 
48	dr.End = time.Time(v.End)
 
52// xtime and its UnmarshalJSON exists to work around a specific invalid date-time encoding seen in the wild.
 
55func (x *xtime) UnmarshalJSON(buf []byte) error {
 
57	err := t.UnmarshalJSON(buf)
 
63	// Microsoft is sending reports with invalid start-datetime/end-datetime (missing
 
66	if err := json.Unmarshal(buf, &s); err != nil {
 
69	t, err = time.Parse("2006-01-02T15:04:05", s)
 
78	Policy         ResultPolicy     `json:"policy"`
 
79	Summary        Summary          `json:"summary"`
 
80	FailureDetails []FailureDetails `json:"failure-details"`
 
83type ResultPolicy struct {
 
84	Type   string   `json:"policy-type"`
 
85	String []string `json:"policy-string"`
 
86	Domain string   `json:"policy-domain"`
 
91	TotalSuccessfulSessionCount int64 `json:"total-successful-session-count"`
 
92	TotalFailureSessionCount    int64 `json:"total-failure-session-count"`
 
95// ResultType represents a TLS error.
 
99// https://www.iana.org/assignments/starttls-validation-result-types/starttls-validation-result-types.xhtml
 
102	ResultSTARTTLSNotSupported    ResultType = "starttls-not-supported"
 
103	ResultCertificateHostMismatch ResultType = "certificate-host-mismatch"
 
104	ResultCertificateExpired      ResultType = "certificate-expired"
 
105	ResultTLSAInvalid             ResultType = "tlsa-invalid"
 
106	ResultDNSSECInvalid           ResultType = "dnssec-invalid"
 
107	ResultDANERequired            ResultType = "dane-required"
 
108	ResultCertificateNotTrusted   ResultType = "certificate-not-trusted"
 
109	ResultSTSPolicyInvalid        ResultType = "sts-policy-invalid"
 
110	ResultSTSWebPKIInvalid        ResultType = "sts-webpki-invalid"
 
111	ResultValidationFailure       ResultType = "validation-failure" // Other error.
 
112	ResultSTSPolicyFetch          ResultType = "sts-policy-fetch-error"
 
115type FailureDetails struct {
 
116	ResultType            ResultType `json:"result-type"`
 
117	SendingMTAIP          string     `json:"sending-mta-ip"`
 
118	ReceivingMXHostname   string     `json:"receiving-mx-hostname"`
 
119	ReceivingMXHelo       string     `json:"receiving-mx-helo"`
 
120	ReceivingIP           string     `json:"receiving-ip"`
 
121	FailedSessionCount    int64      `json:"failed-session-count"`
 
122	AdditionalInformation string     `json:"additional-information"`
 
123	FailureReasonCode     string     `json:"failure-reason-code"`
 
126// Parse parses a Report.
 
127// The maximum size is 20MB.
 
128func Parse(r io.Reader) (*Report, error) {
 
129	r = &moxio.LimitReader{R: r, Limit: 20 * 1024 * 1024}
 
131	if err := json.NewDecoder(r).Decode(&report); err != nil {
 
134	// note: there may be leftover data, we ignore it.
 
138// ParseMessage parses a Report from a mail message.
 
139// The maximum size of the message is 15MB, the maximum size of the
 
140// decompressed report is 20MB.
 
141func ParseMessage(log *mlog.Log, r io.ReaderAt) (*Report, error) {
 
143	p, err := message.Parse(log, true, &moxio.LimitAtReader{R: r, Limit: 15 * 1024 * 1024})
 
145		return nil, fmt.Errorf("parsing mail message: %s", err)
 
148	// Using multipart appears optional, and similar to DMARC someone may decide to
 
149	// send it like that, so accept a report if it's the entire message.
 
151	return parseMessageReport(log, p, allow)
 
154func parseMessageReport(log *mlog.Log, p message.Part, allow bool) (*Report, error) {
 
155	if p.MediaType != "MULTIPART" {
 
157			return nil, ErrNoReport
 
159		return parseReport(p)
 
163		sp, err := p.ParseNextPart(log)
 
165			return nil, ErrNoReport
 
170		if p.MediaSubType == "REPORT" && p.ContentTypeParams["report-type"] != "tlsrpt" {
 
171			return nil, fmt.Errorf("unknown report-type parameter %q", p.ContentTypeParams["report-type"])
 
173		report, err := parseMessageReport(log, *sp, p.MediaSubType == "REPORT")
 
174		if err == ErrNoReport {
 
176		} else if err != nil || report != nil {
 
182func parseReport(p message.Part) (*Report, error) {
 
183	mt := strings.ToLower(p.MediaType + "/" + p.MediaSubType)
 
185	case "application/tlsrpt+json":
 
186		return Parse(p.Reader())
 
187	case "application/tlsrpt+gzip":
 
188		gzr, err := gzip.NewReader(p.Reader())
 
190			return nil, fmt.Errorf("decoding gzip TLSRPT report: %s", err)
 
194	return nil, ErrNoReport