9// Extension is an additional key/value pair for a TLSRPT record.
10type Extension struct {
15// Record is a parsed TLSRPT record, to be served under "_smtp._tls.<domain>".
19// v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;
21 Version string // "TLSRPTv1", for "v=".
22 RUAs [][]string // Aggregate reporting URI, for "rua=". "rua=" can occur multiple times, each can be a list. Must be URL-encoded strings, with ",", "!" and ";" encoded.
23 Extensions []Extension
26// String returns a string or use as a TLSRPT DNS TXT record.
27func (r Record) String() string {
28 b := &strings.Builder{}
29 fmt.Fprint(b, "v="+r.Version)
30 for _, rua := range r.RUAs {
31 fmt.Fprint(b, "; rua="+strings.Join(rua, ","))
33 for _, p := range r.Extensions {
34 fmt.Fprint(b, "; "+p.Key+"="+p.Value)
41func (e parseErr) Error() string {
45var _ error = parseErr("")
47// ParseRecord parses a TLSRPT record.
48func ParseRecord(txt string) (record *Record, istlsrpt bool, err error) {
54 if xerr, ok := x.(parseErr); ok {
56 err = fmt.Errorf("%w: %s", ErrRecordSyntax, xerr)
74 // note: duplicates are allowed.
77 record.RUAs = append(record.RUAs, p.xruas())
80 record.Extensions = append(record.Extensions, Extension{k, v})
82 if !p.delim() || p.empty() {
87 p.xerrorf("leftover chars")
89 if record.RUAs == nil {
90 p.xerrorf("missing rua")
100func newParser(s string) *parser {
104func (p *parser) xerrorf(format string, args ...any) {
105 msg := fmt.Sprintf(format, args...)
107 msg += fmt.Sprintf(" (remain %q)", p.s[p.o:])
112func (p *parser) xtake(s string) string {
114 p.xerrorf("expected %q", s)
120func (p *parser) xdelim() {
122 p.xerrorf("expected semicolon")
126func (p *parser) xtaken(n int) string {
127 r := p.s[p.o : p.o+n]
132func (p *parser) prefix(s string) bool {
133 return strings.HasPrefix(p.s[p.o:], s)
136func (p *parser) take(s string) bool {
144func (p *parser) xtakefn1(fn func(rune, int) bool) string {
145 for i, b := range p.s[p.o:] {
148 p.xerrorf("expected at least one char")
154 p.xerrorf("expected at least 1 char")
156 return p.xtaken(len(p.s) - p.o)
160func (p *parser) xkey() string {
161 return p.xtakefn1(func(b rune, i int) bool {
162 return i < 32 && (b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || (i > 0 && b == '_' || b == '-' || b == '.'))
167func (p *parser) xvalue() string {
168 return p.xtakefn1(func(b rune, i int) bool {
169 return b > ' ' && b < 0x7f && b != '=' && b != ';'
174func (p *parser) delim() bool {
177 for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
180 if o >= e || p.s[o] != ';' {
184 for o < e && (p.s[o] == ' ' || p.s[o] == '\t') {
191func (p *parser) empty() bool {
192 return p.o >= len(p.s)
195func (p *parser) wsp() {
196 for p.o < len(p.s) && (p.s[p.o] == ' ' || p.s[p.o] == '\t') {
202func (p *parser) xruas() []string {
203 l := []string{p.xuri()}
207 l = append(l, p.xuri())
214func (p *parser) xuri() string {
215 v := p.xtakefn1(func(b rune, i int) bool {
216 return b != ',' && b != '!' && b != ' ' && b != '\t' && b != ';'
218 u, err := url.Parse(v)
220 p.xerrorf("parsing uri %q: %s", v, err)
223 p.xerrorf("missing scheme in uri")