-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathreport.go
133 lines (110 loc) · 3.47 KB
/
report.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package main
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
)
type RFC3339OptionalTimezone time.Time
func (t *RFC3339OptionalTimezone) UnmarshalJSON(input []byte) error {
trimmed := strings.Trim(string(input), `"`)
parsedTime, err := time.Parse(time.RFC3339, trimmed)
if err == nil {
*t = RFC3339OptionalTimezone(parsedTime)
return nil
}
RFC3339NoTimeZone := "2006-01-02T15:04:05"
parsedTime, err = time.Parse(RFC3339NoTimeZone, trimmed)
if err == nil {
*t = RFC3339OptionalTimezone(parsedTime)
return nil
}
return fmt.Errorf("invalid time '%s'", trimmed)
}
func (t RFC3339OptionalTimezone) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(t))
}
type DateRange struct {
StartDatetime RFC3339OptionalTimezone `json:"start-datetime"`
EndDatetime RFC3339OptionalTimezone `json:"end-datetime"`
}
type MxHost []string
// NOTE: see https://www.rfc-editor.org/errata/eid6241
func (l *MxHost) UnmarshalJSON(input []byte) error {
if len(input) == 0 {
return nil
}
switch input[0] {
case '"':
*l = MxHost{strings.Trim(string(input), `"`)}
return nil
case '[':
var tmp []string
err := json.Unmarshal(input, &tmp)
if err != nil {
return err
}
*l = MxHost(tmp)
return nil
default:
return fmt.Errorf("invalid mx-host '%s'", string(input))
}
}
type ReportPolicy struct {
PolicyType string `json:"policy-type"`
PolicyString []string `json:"policy-string"`
PolicyDomain string `json:"policy-domain"`
MxHost MxHost `json:"mx-host"`
}
type Summary struct {
TotalSuccessfulSessionCount int64 `json:"total-successful-session-count"`
TotalFailureSessionCount int64 `json:"total-failure-session-count"`
}
type FailureDetail struct {
ResultType string `json:"result-type"`
SendingMtaIp string `json:"sending-mta-ip"`
ReceivingIp string `json:"receiving-ip"`
ReceivingMxHostname string `json:"receiving-mx-hostname"`
FailedSessionCount int64 `json:"failed-session-count"`
FailureReasonCode string `json:"failure-reason-code"`
AdditionalInformation string `json:"additional-information"`
}
type PolicyItem struct {
Policy ReportPolicy `json:"policy"`
Summary Summary `json:"summary"`
FailureDetails []FailureDetail `json:"failure-details"`
}
type Report struct {
OrganizationName string `json:"organization-name"`
DateRange DateRange `json:"date-range"`
ContactInfo string `json:"contact-info"`
ReportId string `json:"report-id"`
Policies []PolicyItem `json:"policies"`
}
func parseReport(reader io.Reader) (Report, error) {
report := &Report{}
err := json.NewDecoder(reader).Decode(report)
return *report, err
}
func saveReport(config Config, reader io.Reader) (io.Reader, *os.File, error) {
err := os.MkdirAll(config.Reports.Save.Path, os.ModePerm)
if err != nil {
slog.Error("Failed to create directory", "path", config.Reports.Save.Path, "error", err)
return reader, nil, err
}
filename := time.Now().Format(time.RFC3339Nano) + ".json"
target := filepath.Join(config.Reports.Save.Path, filename)
slog.Info("Saving report", "target", target)
file, err := os.Create(target)
if err != nil {
slog.Error("Failed to create file", "target", target, "error", err)
return reader, nil, err
}
/// NOTE: It seems `TeeReader` writes the complete stream even when parsing fails later.
/// This is probably only true for small payloads.
return io.TeeReader(reader, file), file, nil
}