implement "future release"

the smtp extension, rfc 4865.
also implement in the webmail.
the queueing/delivery part hardly required changes: we just set the first
delivery time in the future instead of immediately.

still have to find the first client that implements it.
This commit is contained in:
Mechiel Lukkien
2024-02-10 17:55:56 +01:00
parent 17734196e3
commit 93c52b01a0
19 changed files with 382 additions and 54 deletions

View File

@ -6,6 +6,7 @@ import (
"net"
"strconv"
"strings"
"time"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mox-"
@ -137,7 +138,7 @@ func (p *parser) peekchar() rune {
return -1
}
func (p *parser) takefn1(what string, fn func(c rune, i int) bool) string {
func (p *parser) xtakefn1(what string, fn func(c rune, i int) bool) string {
if p.empty() {
p.xerrorf("need at least one char for %s", what)
}
@ -152,7 +153,7 @@ func (p *parser) takefn1(what string, fn func(c rune, i int) bool) string {
return p.remainder()
}
func (p *parser) takefn1case(what string, fn func(c rune, i int) bool) string {
func (p *parser) xtakefn1case(what string, fn func(c rune, i int) bool) string {
if p.empty() {
p.xerrorf("need at least one char for %s", what)
}
@ -167,7 +168,7 @@ func (p *parser) takefn1case(what string, fn func(c rune, i int) bool) string {
return p.remainder()
}
func (p *parser) takefn(fn func(c rune, i int) bool) string {
func (p *parser) xtakefn(fn func(c rune, i int) bool) string {
for i, c := range p.upper[p.o:] {
if !fn(c, i) {
return p.xtaken(i)
@ -183,7 +184,7 @@ func (p *parser) takefn(fn func(c rune, i int) bool) string {
// ../rfc/5321:2260
func (p *parser) xrawReversePath() string {
p.xtake("<")
s := p.takefn(func(c rune, i int) bool {
s := p.xtakefn(func(c rune, i int) bool {
return c != '>'
})
p.xtake(">")
@ -261,7 +262,7 @@ func (p *parser) xdomain() dns.Domain {
// ../rfc/5321:2303
// ../rfc/5321:2303 ../rfc/6531:411
func (p *parser) xsubdomain() string {
return p.takefn1("subdomain", func(c rune, i int) bool {
return p.xtakefn1("subdomain", func(c rune, i int) bool {
return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || i > 0 && c == '-' || c > 0x7f && p.smtputf8
})
}
@ -275,7 +276,7 @@ func (p *parser) xmailbox() smtp.Path {
// ../rfc/5321:2307
func (p *parser) xldhstr() string {
return p.takefn1("ldh-str", func(c rune, i int) bool {
return p.xtakefn1("ldh-str", func(c rune, i int) bool {
return c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || i == 0 && c == '-'
})
}
@ -295,7 +296,7 @@ func (p *parser) xipdomain(isehlo bool) dns.IPDomain {
}
ipv6 = true
}
ipaddr := p.takefn1("address literal", func(c rune, i int) bool {
ipaddr := p.xtakefn1("address literal", func(c rune, i int) bool {
return c != ']'
})
p.take("]")
@ -402,7 +403,7 @@ func (p *parser) xchar() rune {
// ../rfc/5321:2320 ../rfc/6531:414
func (p *parser) xatom(islocalpart bool) string {
return p.takefn1("atom", func(c rune, i int) bool {
return p.xtakefn1("atom", func(c rune, i int) bool {
switch c {
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~':
return true
@ -424,23 +425,23 @@ func (p *parser) xstring() string {
// ../rfc/5321:2279
func (p *parser) xparamKeyword() string {
return p.takefn1("parameter keyword", func(c rune, i int) bool {
return p.xtakefn1("parameter keyword", func(c rune, i int) bool {
return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || (i > 0 && c == '-')
})
}
// ../rfc/5321:2281 ../rfc/6531:422
func (p *parser) xparamValue() string {
return p.takefn1("parameter value", func(c rune, i int) bool {
return p.xtakefn1("parameter value", func(c rune, i int) bool {
return c > ' ' && c < 0x7f && c != '=' || (c > 0x7f && p.smtputf8)
})
}
// for smtp parameters that take a numeric parameter with specified number of
// digits, eg SIZE=... for MAIL FROM.
func (p *parser) xnumber(maxDigits int) int64 {
s := p.takefn1("number", func(c rune, i int) bool {
return c >= '0' && c <= '9' && i < maxDigits
func (p *parser) xnumber(maxDigits int, allowZero bool) int64 {
s := p.xtakefn1("number", func(c rune, i int) bool {
return (c >= '1' && c <= '9' || c == '0' && (i > 0 || allowZero)) && i < maxDigits
})
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
@ -449,10 +450,54 @@ func (p *parser) xnumber(maxDigits int) int64 {
return v
}
// parse date-time in UTC form. ../rfc/4865:147 ../rfc/4865-eid2040
func (p *parser) xdatetimeutc() (time.Time, string) {
// ../rfc/3339:422
xdash := func() string {
p.xtake("-")
return "-"
}
xcolon := func() string {
p.xtake(":")
return ":"
}
xdigits := func(n int) string {
s := p.xtakefn1("digits", func(c rune, i int) bool {
return c >= '0' && c <= '9' && i < n
})
if len(s) != n {
p.xerrorf("parsing date-time: got %d digits, need %d", len(s), n)
}
return s
}
s := xdigits(4) + xdash() + xdigits(2) + xdash() + xdigits(2)
if !p.hasPrefix("T") {
p.xerrorf("expected T for date-time separator")
}
s += p.xtaken(1) + xdigits(2) + xcolon() + xdigits(2) + xcolon() + xdigits(2)
layout := time.RFC3339
if p.take(".") {
layout = time.RFC3339Nano
s += "." + p.xtakefn1("digits", func(c rune, i int) bool {
return c >= '0' && c <= '9'
})
}
if !p.hasPrefix("Z") {
p.xerrorf("expected Z for date-time utc timezone")
}
s += p.xtaken(1)
t, err := time.Parse(layout, s)
if err != nil {
p.xerrorf("bad utc date-time %q: %s", s, err)
}
return t, s
}
// sasl mechanism, for AUTH command.
// ../rfc/4422:436
func (p *parser) xsaslMech() string {
return p.takefn1case("sasl-mech", func(c rune, i int) bool {
return p.xtakefn1case("sasl-mech", func(c rune, i int) bool {
return i < 20 && (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_')
})
}