mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 08:23:48 +03:00
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:
parent
17734196e3
commit
93c52b01a0
19 changed files with 382 additions and 54 deletions
10
dsn/dsn.go
10
dsn/dsn.go
|
@ -48,6 +48,12 @@ type Message struct {
|
|||
// mail user-agents will thread the DSN with the original message.
|
||||
References string
|
||||
|
||||
// For message submitted with FUTURERELEASE SMTP extension. Value is either "for;"
|
||||
// plus original interval in seconds or "until;" plus original UTC RFC3339
|
||||
// date-time.
|
||||
FutureReleaseRequest string
|
||||
// ../rfc/4865:315
|
||||
|
||||
// Human-readable text explaining the failure. Line endings should be
|
||||
// bare newlines, not \r\n. They are converted to \r\n when composing.
|
||||
TextBody string
|
||||
|
@ -230,6 +236,10 @@ func (m *Message) Compose(log mlog.Log, smtputf8 bool) ([]byte, error) {
|
|||
status("Received-From-MTA", fmt.Sprintf("dns;%s (%s)", m.ReceivedFromMTA.Name, smtp.AddressLiteral(m.ReceivedFromMTA.ConnIP)))
|
||||
}
|
||||
status("Arrival-Date", m.ArrivalDate.Format(message.RFC5322Z)) // ../rfc/3464:758
|
||||
if m.FutureReleaseRequest != "" {
|
||||
// ../rfc/4865:320
|
||||
status("Future-Release-Request", m.FutureReleaseRequest)
|
||||
}
|
||||
|
||||
// Then per-recipient fields. ../rfc/3464:769
|
||||
// todo: should also handle other address types. at least recognize "unknown". Probably just store this field. ../rfc/3464:819
|
||||
|
|
|
@ -85,9 +85,10 @@ func TestDSN(t *testing.T) {
|
|||
MessageID: "test@localhost",
|
||||
TextBody: "delivery failure\n",
|
||||
|
||||
ReportingMTA: "mox.example",
|
||||
ReceivedFromMTA: smtp.Ehlo{Name: xparseIPDomain("relay.example"), ConnIP: net.ParseIP("10.10.10.10")},
|
||||
ArrivalDate: now,
|
||||
ReportingMTA: "mox.example",
|
||||
ReceivedFromMTA: smtp.Ehlo{Name: xparseIPDomain("relay.example"), ConnIP: net.ParseIP("10.10.10.10")},
|
||||
ArrivalDate: now,
|
||||
FutureReleaseRequest: "for;123",
|
||||
|
||||
Recipients: []Recipient{
|
||||
{
|
||||
|
|
|
@ -128,8 +128,9 @@ func deliverDSN(ctx context.Context, log mlog.Log, m Msg, remoteMTA dsn.NameIP,
|
|||
References: m.MessageID,
|
||||
TextBody: textBody,
|
||||
|
||||
ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
|
||||
ArrivalDate: m.Queued,
|
||||
ReportingMTA: mox.Conf.Static.HostnameDomain.ASCII,
|
||||
ArrivalDate: m.Queued,
|
||||
FutureReleaseRequest: m.FutureReleaseRequest,
|
||||
|
||||
Recipients: []dsn.Recipient{
|
||||
{
|
||||
|
|
|
@ -67,6 +67,9 @@ var jitter = mox.NewPseudoRand()
|
|||
var DBTypes = []any{Msg{}} // Types stored in DB.
|
||||
var DB *bstore.DB // Exported for making backups.
|
||||
|
||||
// Allow requesting delivery starting from up to this interval from time of submission.
|
||||
const FutureReleaseIntervalMax = 60 * 24 * time.Hour
|
||||
|
||||
// Set for mox localserve, to prevent queueing.
|
||||
var Localserve bool
|
||||
|
||||
|
@ -122,6 +125,12 @@ type Msg struct {
|
|||
// i.e. falling back to SMTP delivery with unverified STARTTLS or plain text.
|
||||
RequireTLS *bool
|
||||
// ../rfc/8689:250
|
||||
|
||||
// For DSNs, where the original FUTURERELEASE value must be included as per-message
|
||||
// field. This field should be of the form "for;" plus interval, or "until;" plus
|
||||
// utc date-time.
|
||||
FutureReleaseRequest string
|
||||
// ../rfc/4865:305
|
||||
}
|
||||
|
||||
// Sender of message as used in MAIL FROM.
|
||||
|
@ -200,6 +209,7 @@ func Count(ctx context.Context) (int, error) {
|
|||
|
||||
// MakeMsg is a convenience function that sets the commonly used fields for a Msg.
|
||||
func MakeMsg(senderAccount string, sender, recipient smtp.Path, has8bit, smtputf8 bool, size int64, messageID string, prefix []byte, requireTLS *bool) Msg {
|
||||
now := time.Now()
|
||||
return Msg{
|
||||
SenderAccount: mox.Conf.Static.Postmaster.Account,
|
||||
SenderLocalpart: sender.Localpart,
|
||||
|
@ -212,6 +222,9 @@ func MakeMsg(senderAccount string, sender, recipient smtp.Path, has8bit, smtputf
|
|||
MessageID: messageID,
|
||||
MsgPrefix: prefix,
|
||||
RequireTLS: requireTLS,
|
||||
Queued: now,
|
||||
NextAttempt: now,
|
||||
RecipientDomainStr: formatIPDomain(recipient.IPDomain),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,12 +241,6 @@ func Add(ctx context.Context, log mlog.Log, qm *Msg, msgFile *os.File) error {
|
|||
if qm.ID != 0 {
|
||||
return fmt.Errorf("id of queued message must be 0")
|
||||
}
|
||||
qm.Queued = time.Now()
|
||||
qm.DialedIPs = nil
|
||||
qm.NextAttempt = qm.Queued
|
||||
qm.LastAttempt = nil
|
||||
qm.LastError = ""
|
||||
qm.RecipientDomainStr = formatIPDomain(qm.RecipientDomain)
|
||||
|
||||
if Localserve {
|
||||
if qm.SenderAccount == "" {
|
||||
|
|
|
@ -66,7 +66,8 @@ https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
|||
3974 - - SMTP Operational Experience in Mixed IPv4/v6 Environments
|
||||
4409 - Obs (RFC 6409) Message Submission for Mail
|
||||
4468 Roadmap - Message Submission BURL Extension
|
||||
4865 Roadmap - SMTP Submission Service Extension for Future Message Release
|
||||
4865 Yes - SMTP Submission Service Extension for Future Message Release
|
||||
4865-eid2040 - errata: Internet-style-date-time-UTC -> date-time from rfc 3339
|
||||
4954 Yes - SMTP Service Extension for Authentication
|
||||
5068 - - Email Submission Operations: Access and Accountability Requirements
|
||||
5248 - - A Registry for SMTP Enhanced Mail System Status Codes
|
||||
|
@ -83,6 +84,7 @@ https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
|||
6532 Yes - Internationalized Email Headers
|
||||
6533 Yes - Internationalized Delivery Status and Disposition Notifications
|
||||
6647 Partial - Email Greylisting: An Applicability Statement for SMTP
|
||||
6710 No - Simple Mail Transfer Protocol Extension for Message Transfer Priorities
|
||||
6729 No - Indicating Email Handling States in Trace Fields
|
||||
6857 No - Post-Delivery Message Downgrading for Internationalized Email Messages
|
||||
7293 No - The Require-Recipient-Valid-Since Header Field and SMTP Service Extension
|
||||
|
|
|
@ -132,14 +132,16 @@ var (
|
|||
SePol7PasswdTransitionReq12 = "7.12" // ../rfc/4954:578
|
||||
SePol7AccountDisabled13 = "7.13" // ../rfc/5248:399
|
||||
SePol7TrustReq14 = "7.14" // ../rfc/5248:418
|
||||
SePol7NoDKIMPass20 = "7.20" // ../rfc/7372:137
|
||||
SePol7NoDKIMAccept21 = "7.21" // ../rfc/7372:148
|
||||
SePol7NoDKIMAuthorMatch22 = "7.22" // ../rfc/7372:175
|
||||
SePol7SPFResultFail23 = "7.23" // ../rfc/7372:192
|
||||
SePol7SPFError24 = "7.24" // ../rfc/7372:204
|
||||
SePol7RevDNSFail25 = "7.25" // ../rfc/7372:233
|
||||
SePol7MultiAuthFails26 = "7.26" // ../rfc/7372:246
|
||||
SePol7SenderHasNullMX27 = "7.27" // ../rfc/7505:246
|
||||
SePol7ARCFail = "7.29" // ../rfc/8617:1438
|
||||
SePol7MissingReqTLS = "7.30" // ../rfc/8689:448
|
||||
// todo spec: duplicate spec of 7.16 ../rfc/4865:412 ../rfc/6710:878
|
||||
// todo spec: duplicate spec of 7.17 ../rfc/4865:418 ../rfc/7293:1137
|
||||
SePol7NoDKIMPass20 = "7.20" // ../rfc/7372:137
|
||||
SePol7NoDKIMAccept21 = "7.21" // ../rfc/7372:148
|
||||
SePol7NoDKIMAuthorMatch22 = "7.22" // ../rfc/7372:175
|
||||
SePol7SPFResultFail23 = "7.23" // ../rfc/7372:192
|
||||
SePol7SPFError24 = "7.24" // ../rfc/7372:204
|
||||
SePol7RevDNSFail25 = "7.25" // ../rfc/7372:233
|
||||
SePol7MultiAuthFails26 = "7.26" // ../rfc/7372:246
|
||||
SePol7SenderHasNullMX27 = "7.27" // ../rfc/7505:246
|
||||
SePol7ARCFail = "7.29" // ../rfc/8617:1438
|
||||
SePol7MissingReqTLS = "7.30" // ../rfc/8689:448
|
||||
)
|
||||
|
|
|
@ -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 == '_')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -321,11 +321,13 @@ type conn struct {
|
|||
transactionBad int
|
||||
|
||||
// Message transaction.
|
||||
mailFrom *smtp.Path
|
||||
requireTLS *bool // MAIL FROM with REQUIRETLS set.
|
||||
has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
|
||||
smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart. we should decide ourselves if the message needs smtputf8, e.g. due to utf8 header values.
|
||||
recipients []rcptAccount
|
||||
mailFrom *smtp.Path
|
||||
requireTLS *bool // MAIL FROM with REQUIRETLS set.
|
||||
futureRelease time.Time // MAIL FROM with HOLDFOR or HOLDUNTIL.
|
||||
futureReleaseRequest string // For use in DSNs, either "for;" or "until;" plus original value. ../rfc/4865:305
|
||||
has8bitmime bool // If MAIL FROM parameter BODY=8BITMIME was sent. Required for SMTPUTF8.
|
||||
smtputf8 bool // todo future: we should keep track of this per recipient. perhaps only a specific recipient requires smtputf8, e.g. due to a utf8 localpart. we should decide ourselves if the message needs smtputf8, e.g. due to utf8 header values.
|
||||
recipients []rcptAccount
|
||||
}
|
||||
|
||||
type rcptAccount struct {
|
||||
|
@ -361,6 +363,8 @@ func (c *conn) reset() {
|
|||
func (c *conn) rset() {
|
||||
c.mailFrom = nil
|
||||
c.requireTLS = nil
|
||||
c.futureRelease = time.Time{}
|
||||
c.futureReleaseRequest = ""
|
||||
c.has8bitmime = false
|
||||
c.smtputf8 = false
|
||||
c.recipients = nil
|
||||
|
@ -878,6 +882,9 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
|||
} else {
|
||||
c.bwritelinef("250-AUTH ")
|
||||
}
|
||||
// ../rfc/4865:127
|
||||
t := time.Now().Add(queue.FutureReleaseIntervalMax).UTC() // ../rfc/4865:98
|
||||
c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
|
||||
}
|
||||
c.bwritelinef("250-ENHANCEDSTATUSCODES") // ../rfc/2034:71
|
||||
// todo future? c.writelinef("250-DSN")
|
||||
|
@ -1306,7 +1313,7 @@ func (c *conn) cmdAuth(p *parser) {
|
|||
func (c *conn) cmdMail(p *parser) {
|
||||
// requirements for maximum line length:
|
||||
// ../rfc/5321:3500 (base max of 512 including crlf) ../rfc/4954:134 (+500) ../rfc/1870:92 (+26) ../rfc/6152:90 (none specified) ../rfc/6531:231 (+10)
|
||||
// todo future: enforce?
|
||||
// todo future: enforce? doesn't really seem worth it...
|
||||
|
||||
if c.transactionBad > 10 && c.transactionGood == 0 {
|
||||
// If we get many bad transactions, it's probably a spammer that is guessing user names.
|
||||
|
@ -1354,7 +1361,7 @@ func (c *conn) cmdMail(p *parser) {
|
|||
switch K {
|
||||
case "SIZE":
|
||||
p.xtake("=")
|
||||
size := p.xnumber(20) // ../rfc/1870:90
|
||||
size := p.xnumber(20, true) // ../rfc/1870:90
|
||||
if size > c.maxMessageSize {
|
||||
// ../rfc/1870:136 ../rfc/3463:382
|
||||
ecode := smtp.SeSys3MsgLimitExceeded4
|
||||
|
@ -1402,6 +1409,39 @@ func (c *conn) cmdMail(p *parser) {
|
|||
}
|
||||
v := true
|
||||
c.requireTLS = &v
|
||||
case "HOLDFOR", "HOLDUNTIL":
|
||||
// Only for submission ../rfc/4865:163
|
||||
if !c.submission {
|
||||
xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
|
||||
}
|
||||
if K == "HOLDFOR" && paramSeen["HOLDUNTIL"] || K == "HOLDUNTIL" && paramSeen["HOLDFOR"] {
|
||||
// ../rfc/4865:260
|
||||
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "cannot use both HOLDUNTIL and HOLFOR")
|
||||
}
|
||||
p.xtake("=")
|
||||
// ../rfc/4865:263 ../rfc/4865:267 We are not following the advice of treating
|
||||
// semantic errors as syntax errors
|
||||
if K == "HOLDFOR" {
|
||||
n := p.xnumber(9, false) // ../rfc/4865:92
|
||||
if n > int64(queue.FutureReleaseIntervalMax/time.Second) {
|
||||
// ../rfc/4865:250
|
||||
xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "future release interval too far in the future")
|
||||
}
|
||||
c.futureRelease = time.Now().Add(time.Duration(n) * time.Second)
|
||||
c.futureReleaseRequest = fmt.Sprintf("for;%d", n)
|
||||
} else {
|
||||
t, s := p.xdatetimeutc()
|
||||
ival := time.Until(t)
|
||||
if ival <= 0 {
|
||||
// Likely a mistake by the user.
|
||||
xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is in the past")
|
||||
} else if ival > queue.FutureReleaseIntervalMax {
|
||||
// ../rfc/4865:255
|
||||
xsmtpUserErrorf(smtp.C554TransactionFailed, smtp.SeProto5BadParams4, "requested future release time is too far in the future")
|
||||
}
|
||||
c.futureRelease = t
|
||||
c.futureReleaseRequest = "until;" + s
|
||||
}
|
||||
default:
|
||||
// ../rfc/5321:2230
|
||||
xsmtpUserErrorf(smtp.C555UnrecognizedAddrParams, smtp.SeSys3NotSupported3, "unrecognized parameter %q", key)
|
||||
|
@ -1938,6 +1978,11 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
|||
|
||||
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
|
||||
qm := queue.MakeMsg(c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS)
|
||||
if !c.futureRelease.IsZero() {
|
||||
qm.NextAttempt = c.futureRelease
|
||||
qm.FutureReleaseRequest = c.futureReleaseRequest
|
||||
}
|
||||
// todo: it would be good to have a limit on messages (count and total size) a user has in the queue. also/especially with futurerelease. ../rfc/4865:387
|
||||
if err := queue.Add(ctx, c.log, &qm, dataFile); err != nil {
|
||||
// Aborting the transaction is not great. But continuing and generating DSNs will
|
||||
// probably result in errors as well...
|
||||
|
|
|
@ -1658,3 +1658,75 @@ func TestSmuggle(t *testing.T) {
|
|||
test("\r.\r")
|
||||
test("\n.\r\n")
|
||||
}
|
||||
|
||||
func TestFutureRelease(t *testing.T) {
|
||||
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
|
||||
ts.tlsmode = smtpclient.TLSSkip
|
||||
ts.user = "mjl@mox.example"
|
||||
ts.pass = "testtest"
|
||||
ts.submission = true
|
||||
defer ts.close()
|
||||
|
||||
ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientPlain(ts.user, ts.pass), nil
|
||||
}
|
||||
|
||||
test := func(mailtoMore, expResponsePrefix string) {
|
||||
t.Helper()
|
||||
|
||||
ts.runRaw(func(conn net.Conn) {
|
||||
t.Helper()
|
||||
|
||||
ourHostname := mox.Conf.Static.HostnameDomain
|
||||
remoteHostname := dns.Domain{ASCII: "mox.example"}
|
||||
opts := smtpclient.Opts{Auth: ts.auth}
|
||||
log := pkglog.WithCid(ts.cid - 1)
|
||||
_, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, false, ourHostname, remoteHostname, opts)
|
||||
tcheck(t, err, "smtpclient")
|
||||
defer conn.Close()
|
||||
|
||||
write := func(s string) {
|
||||
_, err := conn.Write([]byte(s))
|
||||
tcheck(t, err, "write")
|
||||
}
|
||||
|
||||
readPrefixLine := func(prefix string) string {
|
||||
t.Helper()
|
||||
buf := make([]byte, 512)
|
||||
n, err := conn.Read(buf)
|
||||
tcheck(t, err, "read")
|
||||
s := strings.TrimRight(string(buf[:n]), "\r\n")
|
||||
if !strings.HasPrefix(s, prefix) {
|
||||
t.Fatalf("got smtp response %q, expected line with prefix %q", s, prefix)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
write(fmt.Sprintf("MAIL FROM:<mjl@mox.example>%s\r\n", mailtoMore))
|
||||
readPrefixLine(expResponsePrefix)
|
||||
if expResponsePrefix != "2" {
|
||||
return
|
||||
}
|
||||
write("RCPT TO:<mjl@mox.example>\r\n")
|
||||
readPrefixLine("2")
|
||||
|
||||
write("DATA\r\n")
|
||||
readPrefixLine("3")
|
||||
write("From: <mjl@mox.example>\r\n\r\nbody\r\n\r\n.\r\n")
|
||||
readPrefixLine("2")
|
||||
})
|
||||
}
|
||||
|
||||
test(" HOLDFOR=1", "2")
|
||||
test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339), "2")
|
||||
test(" HOLDUNTIL="+time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), "2")
|
||||
|
||||
test(" HOLDFOR=0", "501") // 0 is invalid syntax.
|
||||
test(fmt.Sprintf(" HOLDFOR=%d", int64((queue.FutureReleaseIntervalMax+time.Minute)/time.Second)), "554") // Too far in the future.
|
||||
test(" HOLDUNTIL="+time.Now().Add(-time.Minute).UTC().Format(time.RFC3339), "554") // In the past.
|
||||
test(" HOLDUNTIL="+time.Now().Add(queue.FutureReleaseIntervalMax+time.Minute).UTC().Format(time.RFC3339), "554") // Too far in the future.
|
||||
test(" HOLDUNTIL=2024-02-10T17:28:00+00:00", "501") // "Z" required.
|
||||
test(" HOLDUNTIL=24-02-10T17:28:00Z", "501") // Invalid.
|
||||
test(" HOLDFOR=1 HOLDFOR=1", "501") // Duplicate.
|
||||
test(" HOLDFOR=1 HOLDUNTIL="+time.Now().Add(time.Hour).UTC().Format(time.RFC3339), "501") // Duplicate.
|
||||
}
|
||||
|
|
|
@ -393,7 +393,7 @@ var api;
|
|||
"Reverse": { "Name": "Reverse", "Docs": "", "Fields": [{ "Name": "Hostnames", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"ClientConfigs": { "Name": "ClientConfigs", "Docs": "", "Fields": [{ "Name": "Entries", "Docs": "", "Typewords": ["[]", "ClientConfigsEntry"] }] },
|
||||
"ClientConfigsEntry": { "Name": "ClientConfigsEntry", "Docs": "", "Fields": [{ "Name": "Protocol", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Port", "Docs": "", "Typewords": ["int32"] }, { "Name": "Listener", "Docs": "", "Typewords": ["string"] }, { "Name": "Note", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Msg": { "Name": "Msg", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Queued", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "SenderAccount", "Docs": "", "Typewords": ["string"] }, { "Name": "SenderLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "SenderDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "RecipientLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "RecipientDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "RecipientDomainStr", "Docs": "", "Typewords": ["string"] }, { "Name": "Attempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "DialedIPs", "Docs": "", "Typewords": ["{}", "[]", "IP"] }, { "Name": "NextAttempt", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastAttempt", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "LastError", "Docs": "", "Typewords": ["string"] }, { "Name": "Has8bit", "Docs": "", "Typewords": ["bool"] }, { "Name": "SMTPUTF8", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsDMARCReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsTLSReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgPrefix", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "DSNUTF8", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }] },
|
||||
"Msg": { "Name": "Msg", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Queued", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "SenderAccount", "Docs": "", "Typewords": ["string"] }, { "Name": "SenderLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "SenderDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "RecipientLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "RecipientDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "RecipientDomainStr", "Docs": "", "Typewords": ["string"] }, { "Name": "Attempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "DialedIPs", "Docs": "", "Typewords": ["{}", "[]", "IP"] }, { "Name": "NextAttempt", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastAttempt", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "LastError", "Docs": "", "Typewords": ["string"] }, { "Name": "Has8bit", "Docs": "", "Typewords": ["bool"] }, { "Name": "SMTPUTF8", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsDMARCReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsTLSReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgPrefix", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "DSNUTF8", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureReleaseRequest", "Docs": "", "Typewords": ["string"] }] },
|
||||
"IPDomain": { "Name": "IPDomain", "Docs": "", "Fields": [{ "Name": "IP", "Docs": "", "Typewords": ["IP"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"WebserverConfig": { "Name": "WebserverConfig", "Docs": "", "Fields": [{ "Name": "WebDNSDomainRedirects", "Docs": "", "Typewords": ["[]", "[]", "Domain"] }, { "Name": "WebDomainRedirects", "Docs": "", "Typewords": ["[]", "[]", "string"] }, { "Name": "WebHandlers", "Docs": "", "Typewords": ["[]", "WebHandler"] }] },
|
||||
"WebHandler": { "Name": "WebHandler", "Docs": "", "Fields": [{ "Name": "LogName", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "PathRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "DontRedirectPlainHTTP", "Docs": "", "Typewords": ["bool"] }, { "Name": "Compress", "Docs": "", "Typewords": ["bool"] }, { "Name": "WebStatic", "Docs": "", "Typewords": ["nullable", "WebStatic"] }, { "Name": "WebRedirect", "Docs": "", "Typewords": ["nullable", "WebRedirect"] }, { "Name": "WebForward", "Docs": "", "Typewords": ["nullable", "WebForward"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
|
|
|
@ -3489,6 +3489,13 @@
|
|||
"nullable",
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "FutureReleaseRequest",
|
||||
"Docs": "For DSNs, where the original FUTURERELEASE value must be included as per-message field. This field should be of the form \"for;\" plus interval, or \"until;\" plus utc date-time.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -494,6 +494,7 @@ export interface Msg {
|
|||
DSNUTF8?: string | null // If set, this message is a DSN and this is a version using utf-8, for the case the remote MTA supports smtputf8. In this case, Size and MsgPrefix are not relevant.
|
||||
Transport: string // If non-empty, the transport to use for this message. Can be set through cli or admin interface. If empty (the default for a submitted message), regular routing rules apply.
|
||||
RequireTLS?: boolean | null // RequireTLS influences TLS verification during delivery. If nil, the recipient domain policy is followed (MTA-STS and/or DANE), falling back to optional opportunistic non-verified STARTTLS. If RequireTLS is true (through SMTP REQUIRETLS extension or webmail submit), MTA-STS or DANE is required, as well as REQUIRETLS support by the next hop server. If RequireTLS is false (through messag header "TLS-Required: No"), the recipient domain's policy is ignored if it does not lead to a successful TLS connection, i.e. falling back to SMTP delivery with unverified STARTTLS or plain text.
|
||||
FutureReleaseRequest: string // For DSNs, where the original FUTURERELEASE value must be included as per-message field. This field should be of the form "for;" plus interval, or "until;" plus utc date-time.
|
||||
}
|
||||
|
||||
// IPDomain is an ip address, a domain, or empty.
|
||||
|
@ -836,7 +837,7 @@ export const types: TypenameMap = {
|
|||
"Reverse": {"Name":"Reverse","Docs":"","Fields":[{"Name":"Hostnames","Docs":"","Typewords":["[]","string"]}]},
|
||||
"ClientConfigs": {"Name":"ClientConfigs","Docs":"","Fields":[{"Name":"Entries","Docs":"","Typewords":["[]","ClientConfigsEntry"]}]},
|
||||
"ClientConfigsEntry": {"Name":"ClientConfigsEntry","Docs":"","Fields":[{"Name":"Protocol","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["Domain"]},{"Name":"Port","Docs":"","Typewords":["int32"]},{"Name":"Listener","Docs":"","Typewords":["string"]},{"Name":"Note","Docs":"","Typewords":["string"]}]},
|
||||
"Msg": {"Name":"Msg","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Queued","Docs":"","Typewords":["timestamp"]},{"Name":"SenderAccount","Docs":"","Typewords":["string"]},{"Name":"SenderLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"SenderDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"RecipientLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"RecipientDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"RecipientDomainStr","Docs":"","Typewords":["string"]},{"Name":"Attempts","Docs":"","Typewords":["int32"]},{"Name":"MaxAttempts","Docs":"","Typewords":["int32"]},{"Name":"DialedIPs","Docs":"","Typewords":["{}","[]","IP"]},{"Name":"NextAttempt","Docs":"","Typewords":["timestamp"]},{"Name":"LastAttempt","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"LastError","Docs":"","Typewords":["string"]},{"Name":"Has8bit","Docs":"","Typewords":["bool"]},{"Name":"SMTPUTF8","Docs":"","Typewords":["bool"]},{"Name":"IsDMARCReport","Docs":"","Typewords":["bool"]},{"Name":"IsTLSReport","Docs":"","Typewords":["bool"]},{"Name":"Size","Docs":"","Typewords":["int64"]},{"Name":"MessageID","Docs":"","Typewords":["string"]},{"Name":"MsgPrefix","Docs":"","Typewords":["nullable","string"]},{"Name":"DSNUTF8","Docs":"","Typewords":["nullable","string"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]}]},
|
||||
"Msg": {"Name":"Msg","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Queued","Docs":"","Typewords":["timestamp"]},{"Name":"SenderAccount","Docs":"","Typewords":["string"]},{"Name":"SenderLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"SenderDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"RecipientLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"RecipientDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"RecipientDomainStr","Docs":"","Typewords":["string"]},{"Name":"Attempts","Docs":"","Typewords":["int32"]},{"Name":"MaxAttempts","Docs":"","Typewords":["int32"]},{"Name":"DialedIPs","Docs":"","Typewords":["{}","[]","IP"]},{"Name":"NextAttempt","Docs":"","Typewords":["timestamp"]},{"Name":"LastAttempt","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"LastError","Docs":"","Typewords":["string"]},{"Name":"Has8bit","Docs":"","Typewords":["bool"]},{"Name":"SMTPUTF8","Docs":"","Typewords":["bool"]},{"Name":"IsDMARCReport","Docs":"","Typewords":["bool"]},{"Name":"IsTLSReport","Docs":"","Typewords":["bool"]},{"Name":"Size","Docs":"","Typewords":["int64"]},{"Name":"MessageID","Docs":"","Typewords":["string"]},{"Name":"MsgPrefix","Docs":"","Typewords":["nullable","string"]},{"Name":"DSNUTF8","Docs":"","Typewords":["nullable","string"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]},{"Name":"FutureReleaseRequest","Docs":"","Typewords":["string"]}]},
|
||||
"IPDomain": {"Name":"IPDomain","Docs":"","Fields":[{"Name":"IP","Docs":"","Typewords":["IP"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||
"WebserverConfig": {"Name":"WebserverConfig","Docs":"","Fields":[{"Name":"WebDNSDomainRedirects","Docs":"","Typewords":["[]","[]","Domain"]},{"Name":"WebDomainRedirects","Docs":"","Typewords":["[]","[]","string"]},{"Name":"WebHandlers","Docs":"","Typewords":["[]","WebHandler"]}]},
|
||||
"WebHandler": {"Name":"WebHandler","Docs":"","Fields":[{"Name":"LogName","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"PathRegexp","Docs":"","Typewords":["string"]},{"Name":"DontRedirectPlainHTTP","Docs":"","Typewords":["bool"]},{"Name":"Compress","Docs":"","Typewords":["bool"]},{"Name":"WebStatic","Docs":"","Typewords":["nullable","WebStatic"]},{"Name":"WebRedirect","Docs":"","Typewords":["nullable","WebRedirect"]},{"Name":"WebForward","Docs":"","Typewords":["nullable","WebForward"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]}]},
|
||||
|
|
|
@ -201,10 +201,11 @@ type SubmitMessage struct {
|
|||
Attachments []File
|
||||
ForwardAttachments ForwardAttachments
|
||||
IsForward bool
|
||||
ResponseMessageID int64 // If set, this was a reply or forward, based on IsForward.
|
||||
ReplyTo string // If non-empty, Reply-To header to add to message.
|
||||
UserAgent string // User-Agent header added if not empty.
|
||||
RequireTLS *bool // For "Require TLS" extension during delivery.
|
||||
ResponseMessageID int64 // If set, this was a reply or forward, based on IsForward.
|
||||
ReplyTo string // If non-empty, Reply-To header to add to message.
|
||||
UserAgent string // User-Agent header added if not empty.
|
||||
RequireTLS *bool // For "Require TLS" extension during delivery.
|
||||
FutureRelease *time.Time // If set, time (in the future) when message should be delivered from queue.
|
||||
}
|
||||
|
||||
// ForwardAttachments references attachments by a list of message.Part paths.
|
||||
|
@ -635,6 +636,17 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
|||
IPDomain: dns.IPDomain{Domain: rcpt.Domain},
|
||||
}
|
||||
qm := queue.MakeMsg(reqInfo.AccountName, fromPath, toPath, has8bit, smtputf8, msgSize, messageID, []byte(rcptMsgPrefix), m.RequireTLS)
|
||||
if m.FutureRelease != nil {
|
||||
ival := time.Until(*m.FutureRelease)
|
||||
if ival < 0 {
|
||||
xcheckuserf(ctx, errors.New("date/time is in the past"), "scheduling delivery")
|
||||
} else if ival > queue.FutureReleaseIntervalMax {
|
||||
xcheckuserf(ctx, fmt.Errorf("date/time can not be further than %v in the future", queue.FutureReleaseIntervalMax), "scheduling delivery")
|
||||
}
|
||||
qm.NextAttempt = *m.FutureRelease
|
||||
qm.FutureReleaseRequest = "until;" + m.FutureRelease.Format(time.RFC3339)
|
||||
// todo: possibly add a header to the message stored in the Sent mailbox to indicate it was scheduled for later delivery.
|
||||
}
|
||||
err := queue.Add(ctx, log, &qm, dataFile)
|
||||
if err != nil {
|
||||
metricSubmission.WithLabelValues("queueerror").Inc()
|
||||
|
|
|
@ -1169,6 +1169,14 @@
|
|||
"nullable",
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "FutureRelease",
|
||||
"Docs": "If set, time (in the future) when message should be delivered from queue.",
|
||||
"Typewords": [
|
||||
"nullable",
|
||||
"timestamp"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -144,6 +144,7 @@ export interface SubmitMessage {
|
|||
ReplyTo: string // If non-empty, Reply-To header to add to message.
|
||||
UserAgent: string // User-Agent header added if not empty.
|
||||
RequireTLS?: boolean | null // For "Require TLS" extension during delivery.
|
||||
FutureRelease?: Date | null // If set, time (in the future) when message should be delivered from queue.
|
||||
}
|
||||
|
||||
// File is a new attachment (not from an existing message that is being
|
||||
|
@ -531,7 +532,7 @@ export const types: TypenameMap = {
|
|||
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["string"]}]},
|
||||
"MessageAddress": {"Name":"MessageAddress","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||
"Domain": {"Name":"Domain","Docs":"","Fields":[{"Name":"ASCII","Docs":"","Typewords":["string"]},{"Name":"Unicode","Docs":"","Typewords":["string"]}]},
|
||||
"SubmitMessage": {"Name":"SubmitMessage","Docs":"","Fields":[{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Cc","Docs":"","Typewords":["[]","string"]},{"Name":"Bcc","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"TextBody","Docs":"","Typewords":["string"]},{"Name":"Attachments","Docs":"","Typewords":["[]","File"]},{"Name":"ForwardAttachments","Docs":"","Typewords":["ForwardAttachments"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ResponseMessageID","Docs":"","Typewords":["int64"]},{"Name":"ReplyTo","Docs":"","Typewords":["string"]},{"Name":"UserAgent","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]}]},
|
||||
"SubmitMessage": {"Name":"SubmitMessage","Docs":"","Fields":[{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Cc","Docs":"","Typewords":["[]","string"]},{"Name":"Bcc","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"TextBody","Docs":"","Typewords":["string"]},{"Name":"Attachments","Docs":"","Typewords":["[]","File"]},{"Name":"ForwardAttachments","Docs":"","Typewords":["ForwardAttachments"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ResponseMessageID","Docs":"","Typewords":["int64"]},{"Name":"ReplyTo","Docs":"","Typewords":["string"]},{"Name":"UserAgent","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]},{"Name":"FutureRelease","Docs":"","Typewords":["nullable","timestamp"]}]},
|
||||
"File": {"Name":"File","Docs":"","Fields":[{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"DataURI","Docs":"","Typewords":["string"]}]},
|
||||
"ForwardAttachments": {"Name":"ForwardAttachments","Docs":"","Fields":[{"Name":"MessageID","Docs":"","Typewords":["int64"]},{"Name":"Paths","Docs":"","Typewords":["[]","[]","int32"]}]},
|
||||
"Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]},
|
||||
|
|
|
@ -286,7 +286,7 @@ var api;
|
|||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }] },
|
||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }] },
|
||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||
|
|
|
@ -286,7 +286,7 @@ var api;
|
|||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }] },
|
||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }] },
|
||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||
|
|
|
@ -286,7 +286,7 @@ var api;
|
|||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }] },
|
||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }] },
|
||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||
|
@ -2287,6 +2287,7 @@ const compose = (opts) => {
|
|||
IsForward: opts.isForward || false,
|
||||
ResponseMessageID: opts.responseMessageID || 0,
|
||||
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
||||
FutureRelease: scheduleTime.value ? new Date(scheduleTime.value) : null,
|
||||
};
|
||||
await client.MessageSubmit(message);
|
||||
cmdCancel();
|
||||
|
@ -2513,6 +2514,18 @@ const compose = (opts) => {
|
|||
fromOptions.unshift(o);
|
||||
}
|
||||
}
|
||||
let scheduleLink;
|
||||
let scheduleElem;
|
||||
let scheduleTime;
|
||||
let scheduleWeekday;
|
||||
const pad0 = (v) => v >= 10 ? '' + v : '0' + v;
|
||||
const localdate = (d) => [d.getFullYear(), pad0(d.getMonth() + 1), pad0(d.getDate())].join('-');
|
||||
const localdatetime = (d) => localdate(d) + 'T' + pad0(d.getHours()) + ':' + pad0(d.getMinutes()) + ':00';
|
||||
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const scheduleTimeChanged = () => {
|
||||
console.log('datetime change', scheduleTime.value);
|
||||
dom._kids(scheduleWeekday, weekdays[new Date(scheduleTime.value).getDay()]);
|
||||
};
|
||||
const composeElem = dom.div(style({
|
||||
position: 'fixed',
|
||||
bottom: '1ex',
|
||||
|
@ -2548,7 +2561,42 @@ const compose = (opts) => {
|
|||
return v;
|
||||
}), dom.label(style({ color: '#666' }), dom.input(attr.type('checkbox'), function change(e) {
|
||||
forwardAttachmentViews.forEach(v => v.checkbox.checked = e.target.checked);
|
||||
}), ' (Toggle all)')), noAttachmentsWarning = dom.div(style({ display: 'none', backgroundColor: '#fcd284', padding: '0.15em .25em', margin: '.5em 0' }), 'Message mentions attachments, but no files are attached.'), dom.label(style({ margin: '1ex 0', display: 'block' }), 'Attachments ', attachments = dom.input(attr.type('file'), attr.multiple(''), function change() { checkAttachments(); })), dom.label(style({ margin: '1ex 0', display: 'block' }), attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'), 'TLS ', requiretls = dom.select(dom.option(attr.value(''), 'Default'), dom.option(attr.value('yes'), 'With RequireTLS'), dom.option(attr.value('no'), 'Fallback to insecure'))), dom.div(style({ margin: '3ex 0 1ex 0', display: 'block' }), dom.submitbutton('Send'))), async function submit(e) {
|
||||
}), ' (Toggle all)')), noAttachmentsWarning = dom.div(style({ display: 'none', backgroundColor: '#fcd284', padding: '0.15em .25em', margin: '.5em 0' }), 'Message mentions attachments, but no files are attached.'), dom.label(style({ margin: '1ex 0', display: 'block' }), 'Attachments ', attachments = dom.input(attr.type('file'), attr.multiple(''), function change() { checkAttachments(); })), dom.label(style({ margin: '1ex 0', display: 'block' }), attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'), 'TLS ', requiretls = dom.select(dom.option(attr.value(''), 'Default'), dom.option(attr.value('yes'), 'With RequireTLS'), dom.option(attr.value('no'), 'Fallback to insecure'))), dom.div(scheduleLink = dom.a(attr.href(''), 'Schedule', function click(e) {
|
||||
e.preventDefault();
|
||||
scheduleTime.value = localdatetime(new Date());
|
||||
scheduleTimeChanged();
|
||||
scheduleLink.style.display = 'none';
|
||||
scheduleElem.style.display = '';
|
||||
scheduleTime.setAttribute('required', '');
|
||||
}), scheduleElem = dom.div(style({ display: 'none' }), dom.clickbutton('Start of next day', function click(e) {
|
||||
e.preventDefault();
|
||||
const d = new Date(scheduleTime.value);
|
||||
const nextday = new Date(d.getTime() + 24 * 3600 * 1000);
|
||||
scheduleTime.value = localdate(nextday) + 'T09:00:00';
|
||||
scheduleTimeChanged();
|
||||
}), ' ', dom.clickbutton('+1 hour', function click(e) {
|
||||
e.preventDefault();
|
||||
const d = new Date(scheduleTime.value);
|
||||
scheduleTime.value = localdatetime(new Date(d.getTime() + 3600 * 1000));
|
||||
scheduleTimeChanged();
|
||||
}), ' ', dom.clickbutton('+1 day', function click(e) {
|
||||
e.preventDefault();
|
||||
const d = new Date(scheduleTime.value);
|
||||
scheduleTime.value = localdatetime(new Date(d.getTime() + 24 * 3600 * 1000));
|
||||
scheduleTimeChanged();
|
||||
}), ' ', dom.clickbutton('Now', function click(e) {
|
||||
e.preventDefault();
|
||||
scheduleTime.value = localdatetime(new Date());
|
||||
scheduleTimeChanged();
|
||||
}), ' ', dom.clickbutton('Cancel', function click(e) {
|
||||
e.preventDefault();
|
||||
scheduleLink.style.display = '';
|
||||
scheduleElem.style.display = 'none';
|
||||
scheduleTime.removeAttribute('required');
|
||||
scheduleTime.value = '';
|
||||
}), dom.div(style({ marginTop: '1ex' }), scheduleTime = dom.input(attr.type('datetime-local'), function change() {
|
||||
scheduleTimeChanged();
|
||||
}), ' in local timezone ' + (Intl.DateTimeFormat().resolvedOptions().timeZone || '') + ', ', scheduleWeekday = dom.span()))), dom.div(style({ margin: '3ex 0 1ex 0', display: 'block' }), dom.submitbutton('Send'))), async function submit(e) {
|
||||
e.preventDefault();
|
||||
shortcutCmd(cmdSend, shortcuts);
|
||||
}));
|
||||
|
|
|
@ -1363,6 +1363,7 @@ const compose = (opts: ComposeOptions) => {
|
|||
IsForward: opts.isForward || false,
|
||||
ResponseMessageID: opts.responseMessageID || 0,
|
||||
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
||||
FutureRelease: scheduleTime.value ? new Date(scheduleTime.value) : null,
|
||||
}
|
||||
await client.MessageSubmit(message)
|
||||
cmdCancel()
|
||||
|
@ -1628,6 +1629,19 @@ const compose = (opts: ComposeOptions) => {
|
|||
}
|
||||
}
|
||||
|
||||
let scheduleLink: HTMLElement
|
||||
let scheduleElem: HTMLElement
|
||||
let scheduleTime: HTMLInputElement
|
||||
let scheduleWeekday: HTMLElement
|
||||
const pad0 = (v: number) => v >= 10 ? ''+v : '0'+v
|
||||
const localdate = (d: Date) => [d.getFullYear(), pad0(d.getMonth()+1), pad0(d.getDate())].join('-')
|
||||
const localdatetime = (d: Date) => localdate(d) + 'T' + pad0(d.getHours()) + ':' + pad0(d.getMinutes()) + ':00'
|
||||
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||
const scheduleTimeChanged = () => {
|
||||
console.log('datetime change', scheduleTime.value)
|
||||
dom._kids(scheduleWeekday, weekdays[new Date(scheduleTime.value).getDay()])
|
||||
}
|
||||
|
||||
const composeElem = dom.div(
|
||||
style({
|
||||
position: 'fixed',
|
||||
|
@ -1743,6 +1757,58 @@ const compose = (opts: ComposeOptions) => {
|
|||
dom.option(attr.value('no'), 'Fallback to insecure'),
|
||||
),
|
||||
),
|
||||
dom.div(
|
||||
scheduleLink=dom.a(attr.href(''), 'Schedule', function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
scheduleTime.value = localdatetime(new Date())
|
||||
scheduleTimeChanged()
|
||||
scheduleLink.style.display = 'none'
|
||||
scheduleElem.style.display = ''
|
||||
scheduleTime.setAttribute('required', '')
|
||||
}),
|
||||
scheduleElem=dom.div(
|
||||
style({display: 'none'}),
|
||||
dom.clickbutton('Start of next day', function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const d = new Date(scheduleTime.value)
|
||||
const nextday = new Date(d.getTime() + 24*3600*1000)
|
||||
scheduleTime.value = localdate(nextday) + 'T09:00:00'
|
||||
scheduleTimeChanged()
|
||||
}), ' ',
|
||||
dom.clickbutton('+1 hour', function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const d = new Date(scheduleTime.value)
|
||||
scheduleTime.value = localdatetime(new Date(d.getTime() + 3600*1000))
|
||||
scheduleTimeChanged()
|
||||
}), ' ',
|
||||
dom.clickbutton('+1 day', function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const d = new Date(scheduleTime.value)
|
||||
scheduleTime.value = localdatetime(new Date(d.getTime() + 24*3600*1000))
|
||||
scheduleTimeChanged()
|
||||
}), ' ',
|
||||
dom.clickbutton('Now', function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
scheduleTime.value = localdatetime(new Date())
|
||||
scheduleTimeChanged()
|
||||
}), ' ',
|
||||
dom.clickbutton('Cancel', function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
scheduleLink.style.display = ''
|
||||
scheduleElem.style.display = 'none'
|
||||
scheduleTime.removeAttribute('required')
|
||||
scheduleTime.value = ''
|
||||
}),
|
||||
dom.div(
|
||||
style({marginTop: '1ex'}),
|
||||
scheduleTime=dom.input(attr.type('datetime-local'), function change() {
|
||||
scheduleTimeChanged()
|
||||
}),
|
||||
' in local timezone ' + (Intl.DateTimeFormat().resolvedOptions().timeZone || '') + ', ',
|
||||
scheduleWeekday=dom.span(),
|
||||
),
|
||||
),
|
||||
),
|
||||
dom.div(
|
||||
style({margin: '3ex 0 1ex 0', display: 'block'}),
|
||||
dom.submitbutton('Send'),
|
||||
|
|
Loading…
Reference in a new issue