mirror of
https://github.com/mjl-/mox.git
synced 2025-04-21 21:40:01 +03:00
imapserver: implement MULTIAPPEND extension, rfc 3502
MULTIAPPEND modifies the existing APPEND command to allow multiple messages. it is somewhat more involved than a regular append of a single message since the operation (of adding multiple messages) must be atomic. either all are added, or none are. we check as early as possible if the messages won't cause an over-quota error.
This commit is contained in:
parent
b56d6c4061
commit
78e0c0255f
10 changed files with 286 additions and 106 deletions
|
@ -139,7 +139,7 @@ https://nlnet.nl/project/Mox/.
|
|||
- Automate DNS management, for setup and maintenance, such as DANE/DKIM key rotation
|
||||
- Config options for "transactional email domains", for which mox will only
|
||||
send messages
|
||||
- More IMAP extensions (UNAUTHENTICATE, REPLACE, NOTIFY, MULTIAPPEND, OBJECTID, UIDONLY)
|
||||
- More IMAP extensions (UNAUTHENTICATE, REPLACE, NOTIFY, OBJECTID, UIDONLY)
|
||||
- Encrypted storage of files (email messages, TLS keys), also with per account keys
|
||||
- Recognize common deliverability issues and help postmasters solve them
|
||||
- Calendaring with CalDAV/iCal
|
||||
|
|
|
@ -196,7 +196,8 @@ func (c *Conn) TLSConnectionState() *tls.ConnectionState {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Commandf writes a free-form IMAP command to the server.
|
||||
// Commandf writes a free-form IMAP command to the server. An ending \r\n is
|
||||
// written too.
|
||||
// If tag is empty, a next unique tag is assigned.
|
||||
func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) {
|
||||
defer c.recover(&rerr)
|
||||
|
|
|
@ -205,8 +205,8 @@ func (c *Conn) xrespCode() (string, CodeArg) {
|
|||
c.xspace()
|
||||
destUIDValidity := c.xnzuint32()
|
||||
c.xspace()
|
||||
uid := c.xnzuint32()
|
||||
codeArg = CodeAppendUID{destUIDValidity, uid}
|
||||
uids := c.xuidrange()
|
||||
codeArg = CodeAppendUID{destUIDValidity, uids}
|
||||
case "COPYUID":
|
||||
c.xspace()
|
||||
destUIDValidity := c.xnzuint32()
|
||||
|
|
|
@ -38,6 +38,7 @@ const (
|
|||
CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296
|
||||
CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65
|
||||
CapListMetadata Capability = "LIST-METADTA" // ../rfc/9590:73
|
||||
CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33
|
||||
)
|
||||
|
||||
// Status is the tagged final result of a command.
|
||||
|
@ -111,11 +112,11 @@ func (c CodeUint) CodeString() string {
|
|||
// "APPENDUID" response code.
|
||||
type CodeAppendUID struct {
|
||||
UIDValidity uint32
|
||||
UID uint32
|
||||
UIDs NumRange
|
||||
}
|
||||
|
||||
func (c CodeAppendUID) CodeString() string {
|
||||
return fmt.Sprintf("APPENDUID %d %d", c.UIDValidity, c.UID)
|
||||
return fmt.Sprintf("APPENDUID %d %s", c.UIDValidity, c.UIDs.String())
|
||||
}
|
||||
|
||||
// "COPYUID" response code.
|
||||
|
@ -391,6 +392,13 @@ func ParseNumSet(s string) (ns NumSet, rerr error) {
|
|||
return
|
||||
}
|
||||
|
||||
func ParseUIDRange(s string) (nr NumRange, rerr error) {
|
||||
c := Conn{br: bufio.NewReader(strings.NewReader(s))}
|
||||
defer c.recover(&rerr)
|
||||
nr = c.xuidrange()
|
||||
return
|
||||
}
|
||||
|
||||
// NumRange is a single number or range.
|
||||
type NumRange struct {
|
||||
First uint32 // 0 for "*".
|
||||
|
|
|
@ -46,7 +46,7 @@ func TestAppend(t *testing.T) {
|
|||
|
||||
tc2.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||
tc2.xuntagged(imapclient.UntaggedExists(1))
|
||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 1})
|
||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
|
||||
|
||||
tc.transactf("ok", "noop")
|
||||
uid1 := imapclient.FetchUID(1)
|
||||
|
@ -57,11 +57,11 @@ func TestAppend(t *testing.T) {
|
|||
|
||||
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 ({47+}\r\ncontent-type: just completely invalid;;\r\n\r\ntest)")
|
||||
tc2.xuntagged(imapclient.UntaggedExists(2))
|
||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 2})
|
||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("2")})
|
||||
|
||||
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 ({31+}\r\ncontent-type: text/plain;\n\ntest)")
|
||||
tc2.xuntagged(imapclient.UntaggedExists(3))
|
||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 3})
|
||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("3")})
|
||||
|
||||
// Messages that we cannot parse are marked as application/octet-stream. Perhaps
|
||||
// the imap client knows how to deal with them.
|
||||
|
@ -79,6 +79,15 @@ func TestAppend(t *testing.T) {
|
|||
}
|
||||
tc2.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, xbs}})
|
||||
|
||||
// Multiappend with two messages.
|
||||
tc.transactf("ok", "noop") // Flush pending untagged responses.
|
||||
tc.transactf("ok", "append inbox {6+}\r\ntest\r\n {6+}\r\ntost\r\n")
|
||||
tc.xuntagged(imapclient.UntaggedExists(5))
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4:5")})
|
||||
|
||||
// Cancelled with zero-length message.
|
||||
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n")
|
||||
|
||||
tclimit := startArgs(t, false, false, true, true, "limit")
|
||||
defer tclimit.close()
|
||||
tclimit.client.Login("limit@mox.example", password0)
|
||||
|
@ -89,4 +98,25 @@ func TestAppend(t *testing.T) {
|
|||
// Second message would take account past limit.
|
||||
tclimit.transactf("no", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||
tclimit.xcode("OVERQUOTA")
|
||||
|
||||
// Empty mailbox.
|
||||
tclimit.transactf("ok", `store 1 flags (\deleted)`)
|
||||
tclimit.transactf("ok", "expunge")
|
||||
|
||||
// Multiappend with first message within quota, and second message with sync
|
||||
// literal causing quota error. Request should get error response immediately.
|
||||
tclimit.transactf("no", "append inbox {1+}\r\nx {100000}")
|
||||
tclimit.xcode("OVERQUOTA")
|
||||
|
||||
// Again, but second message now with non-sync literal, which is fully consumed by server.
|
||||
tclimit.client.Commandf("", "append inbox {1+}\r\nx {4000+}")
|
||||
buf := make([]byte, 4000, 4002)
|
||||
for i := range buf {
|
||||
buf[i] = 'x'
|
||||
}
|
||||
buf = append(buf, "\r\n"...)
|
||||
_, err := tclimit.client.Write(buf)
|
||||
tclimit.check(err, "write append message")
|
||||
tclimit.response("no")
|
||||
tclimit.xcode("OVERQUOTA")
|
||||
}
|
||||
|
|
|
@ -120,19 +120,19 @@ func testCondstoreQresync(t *testing.T, qresync bool) {
|
|||
// The ones we insert below will start with modseq 3. So we'll have messages with modseq 1 and 3-6.
|
||||
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||
tc.xuntagged(imapclient.UntaggedExists(4))
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 4})
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")})
|
||||
|
||||
tc.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||
tc.xuntagged()
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 2, UID: 1})
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 2, UIDs: xparseUIDRange("1")})
|
||||
|
||||
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||
tc.xuntagged(imapclient.UntaggedExists(5))
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 5})
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")})
|
||||
|
||||
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||
tc.xuntagged(imapclient.UntaggedExists(6))
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UID: 6})
|
||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")})
|
||||
|
||||
tc2.transactf("ok", "Noop")
|
||||
noflags := imapclient.FetchFlags(nil)
|
||||
|
|
|
@ -28,7 +28,6 @@ non-ASCII UTF-8. Until that's enabled, we do use UTF-7 for mailbox names. See
|
|||
- todo: do not return binary data for a fetch body. at least not for imap4rev1. we should be encoding it as base64?
|
||||
- todo: on expunge we currently remove the message even if other sessions still have a reference to the uid. if they try to query the uid, they'll get an error. we could be nicer and only actually remove the message when the last reference has gone. we could add a new flag to store.Message marking the message as expunged, not give new session access to such messages, and make store remove them at startup, and clean them when the last session referencing the session goes. however, it will get much more complicated. renaming messages would need special handling. and should we do the same for removed mailboxes?
|
||||
- todo: try to recover from syntax errors when the last command line ends with a }, i.e. a literal. we currently abort the entire connection. we may want to read some amount of literal data and continue with a next command.
|
||||
- todo future: more extensions: OBJECTID, MULTISEARCH, REPLACE, NOTIFY, CATENATE, MULTIAPPEND, SORT, THREAD.
|
||||
*/
|
||||
|
||||
import (
|
||||
|
@ -165,12 +164,13 @@ var authFailDelay = time.Second // After authentication failure.
|
|||
// NAMESPACE: ../rfc/2342
|
||||
// COMPRESS=DEFLATE: ../rfc/4978
|
||||
// LIST-METADATA: ../rfc/9590
|
||||
// MULTIAPPEND: ../rfc/3502
|
||||
//
|
||||
// We always announce support for SCRAM PLUS-variants, also on connections without
|
||||
// TLS. The client should not be selecting PLUS variants on non-TLS connections,
|
||||
// instead opting to do the bare SCRAM variant without indicating the server claims
|
||||
// to support the PLUS variant (skipping the server downgrade detection check).
|
||||
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE CREATE-SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE QUOTA QUOTA=RES-STORAGE METADATA SAVEDATE WITHIN NAMESPACE COMPRESS=DEFLATE LIST-METADATA"
|
||||
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ACCEPT LIST-EXTENDED SPECIAL-USE CREATE-SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC STATUS=SIZE QUOTA QUOTA=RES-STORAGE METADATA SAVEDATE WITHIN NAMESPACE COMPRESS=DEFLATE LIST-METADATA MULTIAPPEND"
|
||||
|
||||
type conn struct {
|
||||
cid int64
|
||||
|
@ -3298,145 +3298,278 @@ func flaglist(fl store.Flags, keywords []string) listspace {
|
|||
}
|
||||
|
||||
// Append adds a message to a mailbox.
|
||||
// The MULTIAPPEND extension is implemented, allowing multiple flags/datetime/data
|
||||
// sets.
|
||||
//
|
||||
// State: Authenticated and selected.
|
||||
func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
||||
// Command: ../rfc/9051:3406 ../rfc/6855:204 ../rfc/3501:2527
|
||||
// Examples: ../rfc/9051:3482 ../rfc/3501:2589
|
||||
// Command: ../rfc/9051:3406 ../rfc/6855:204 ../rfc/3501:2527 ../rfc/3502:95
|
||||
// Examples: ../rfc/9051:3482 ../rfc/3501:2589 ../rfc/3502:175
|
||||
|
||||
// Request syntax: ../rfc/9051:6325 ../rfc/6855:219 ../rfc/3501:4547
|
||||
// A message that we've (partially) read from the client, and will be delivering to
|
||||
// the mailbox once we have them all. ../rfc/3502:49
|
||||
type appendMsg struct {
|
||||
storeFlags store.Flags
|
||||
keywords []string
|
||||
time time.Time
|
||||
|
||||
file *os.File // Message file we are appending. Can be nil if we are writing to a nopWriteCloser due to being over quota.
|
||||
path string // Path if an actual file, either a temporary file, or of the message file stored in the account.
|
||||
|
||||
mw *message.Writer
|
||||
m store.Message
|
||||
}
|
||||
|
||||
var appends []*appendMsg
|
||||
var committed bool
|
||||
defer func() {
|
||||
for _, a := range appends {
|
||||
if a.file != nil {
|
||||
err := a.file.Close()
|
||||
c.xsanity(err, "closing APPEND temporary file")
|
||||
}
|
||||
|
||||
if !committed && a.path != "" {
|
||||
err := os.Remove(a.path)
|
||||
c.xsanity(err, "removing APPEND temporary file")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Request syntax: ../rfc/9051:6325 ../rfc/6855:219 ../rfc/3501:4547 ../rfc/3502:218
|
||||
p.xspace()
|
||||
name := p.xmailbox()
|
||||
p.xspace()
|
||||
var storeFlags store.Flags
|
||||
var keywords []string
|
||||
if p.hasPrefix("(") {
|
||||
// Error must be a syntax error, to properly abort the connection due to literal.
|
||||
var err error
|
||||
storeFlags, keywords, err = store.ParseFlagsKeywords(p.xflagList())
|
||||
if err != nil {
|
||||
xsyntaxErrorf("parsing flags: %v", err)
|
||||
|
||||
// Check how much quota space is available. We'll keep track of remaining quota as
|
||||
// we accept multiple messages.
|
||||
quotaMsgMax := c.account.QuotaMessageSize()
|
||||
quotaUnlimited := quotaMsgMax == 0
|
||||
var quotaAvail int64
|
||||
var totalSize int64
|
||||
if !quotaUnlimited {
|
||||
c.account.WithRLock(func() {
|
||||
c.xdbread(func(tx *bstore.Tx) {
|
||||
du := store.DiskUsage{ID: 1}
|
||||
err := tx.Get(&du)
|
||||
xcheckf(err, "get quota disk usage")
|
||||
quotaAvail = quotaMsgMax - du.MessageSize
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var overQuota bool // For response code.
|
||||
var cancel bool // In case we've seen zero-sized message append.
|
||||
|
||||
for {
|
||||
// Append msg early, for potential cleanup.
|
||||
var a appendMsg
|
||||
appends = append(appends, &a)
|
||||
|
||||
if p.hasPrefix("(") {
|
||||
// Error must be a syntax error, to properly abort the connection due to literal.
|
||||
var err error
|
||||
a.storeFlags, a.keywords, err = store.ParseFlagsKeywords(p.xflagList())
|
||||
if err != nil {
|
||||
xsyntaxErrorf("parsing flags: %v", err)
|
||||
}
|
||||
p.xspace()
|
||||
}
|
||||
p.xspace()
|
||||
}
|
||||
var tm time.Time
|
||||
if p.hasPrefix(`"`) {
|
||||
tm = p.xdateTime()
|
||||
p.xspace()
|
||||
} else {
|
||||
tm = time.Now()
|
||||
}
|
||||
// todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
|
||||
// todo: this is only relevant if we also support the CATENATE extension?
|
||||
// ../rfc/6855:204
|
||||
utf8 := p.take("UTF8 (")
|
||||
size, sync := p.xliteralSize(utf8, false)
|
||||
if p.hasPrefix(`"`) {
|
||||
a.time = p.xdateTime()
|
||||
p.xspace()
|
||||
} else {
|
||||
a.time = time.Now()
|
||||
}
|
||||
// todo: only with utf8 should we we accept message headers with utf-8. we currently always accept them.
|
||||
// todo: this is only relevant if we also support the CATENATE extension?
|
||||
// ../rfc/6855:204
|
||||
utf8 := p.take("UTF8 (")
|
||||
size, synclit := p.xliteralSize(utf8, false)
|
||||
|
||||
name = xcheckmailboxname(name, true)
|
||||
c.xdbread(func(tx *bstore.Tx) {
|
||||
c.xmailbox(tx, name, "TRYCREATE")
|
||||
})
|
||||
if sync {
|
||||
c.writelinef("+ ")
|
||||
}
|
||||
if !quotaUnlimited && !overQuota {
|
||||
quotaAvail -= size
|
||||
overQuota = quotaAvail < 0
|
||||
}
|
||||
if size == 0 {
|
||||
cancel = true
|
||||
}
|
||||
|
||||
// Read the message into a temporary file.
|
||||
msgFile, err := store.CreateMessageTemp(c.log, "imap-append")
|
||||
xcheckf(err, "creating temp file for message")
|
||||
defer func() {
|
||||
p := msgFile.Name()
|
||||
err := msgFile.Close()
|
||||
c.xsanity(err, "closing APPEND temporary file")
|
||||
err = os.Remove(p)
|
||||
c.xsanity(err, "removing APPEND temporary file")
|
||||
}()
|
||||
defer c.xtrace(mlog.LevelTracedata)()
|
||||
mw := message.NewWriter(msgFile)
|
||||
msize, err := io.Copy(mw, io.LimitReader(c.br, size))
|
||||
c.xtrace(mlog.LevelTrace) // Restore.
|
||||
if err != nil {
|
||||
// Cannot use xcheckf due to %w handling of errIO.
|
||||
panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
|
||||
}
|
||||
if msize != size {
|
||||
xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
|
||||
}
|
||||
var f io.Writer
|
||||
if synclit {
|
||||
// Check for mailbox on first iteration.
|
||||
if len(appends) <= 1 {
|
||||
name = xcheckmailboxname(name, true)
|
||||
c.xdbread(func(tx *bstore.Tx) {
|
||||
c.xmailbox(tx, name, "TRYCREATE")
|
||||
})
|
||||
}
|
||||
|
||||
if overQuota {
|
||||
// ../rfc/9051:5155 ../rfc/9208:472
|
||||
xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", quotaMsgMax)
|
||||
}
|
||||
|
||||
// ../rfc/3502:140
|
||||
if cancel {
|
||||
xuserErrorf("empty message, cancelling append")
|
||||
}
|
||||
|
||||
// Read the message into a temporary file.
|
||||
var err error
|
||||
a.file, err = store.CreateMessageTemp(c.log, "imap-append")
|
||||
xcheckf(err, "creating temp file for message")
|
||||
a.path = a.file.Name()
|
||||
f = a.file
|
||||
|
||||
c.writelinef("+ ")
|
||||
} else {
|
||||
// We'll discard the message and return an error as soon as we can (possible
|
||||
// synchronizing literal of next message, or after we've seen all messages).
|
||||
if overQuota || cancel {
|
||||
f = io.Discard
|
||||
} else {
|
||||
var err error
|
||||
a.file, err = store.CreateMessageTemp(c.log, "imap-append")
|
||||
xcheckf(err, "creating temp file for message")
|
||||
a.path = a.file.Name()
|
||||
f = a.file
|
||||
}
|
||||
}
|
||||
|
||||
defer c.xtrace(mlog.LevelTracedata)()
|
||||
a.mw = message.NewWriter(f)
|
||||
msize, err := io.Copy(a.mw, io.LimitReader(c.br, size))
|
||||
c.xtrace(mlog.LevelTrace) // Restore.
|
||||
if err != nil {
|
||||
// Cannot use xcheckf due to %w handling of errIO.
|
||||
panic(fmt.Errorf("reading literal message: %s (%w)", err, errIO))
|
||||
}
|
||||
if msize != size {
|
||||
xserverErrorf("read %d bytes for message, expected %d (%w)", msize, size, errIO)
|
||||
}
|
||||
totalSize += msize
|
||||
|
||||
if utf8 {
|
||||
line := c.readline(false)
|
||||
np := newParser(line, c)
|
||||
np.xtake(")")
|
||||
np.xempty()
|
||||
} else {
|
||||
line := c.readline(false)
|
||||
np := newParser(line, c)
|
||||
np.xempty()
|
||||
p = newParser(line, c)
|
||||
if utf8 {
|
||||
p.xtake(")")
|
||||
}
|
||||
|
||||
// The MULTIAPPEND extension allows more appends.
|
||||
if !p.space() {
|
||||
break
|
||||
}
|
||||
}
|
||||
p.xempty()
|
||||
if !sync {
|
||||
name = xcheckmailboxname(name, true)
|
||||
|
||||
name = xcheckmailboxname(name, true)
|
||||
|
||||
if overQuota {
|
||||
// ../rfc/9208:472
|
||||
xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", quotaMsgMax)
|
||||
}
|
||||
|
||||
// ../rfc/3502:140
|
||||
if cancel {
|
||||
xuserErrorf("empty message, cancelling append")
|
||||
}
|
||||
|
||||
var mb store.Mailbox
|
||||
var m store.Message
|
||||
var pendingChanges []store.Change
|
||||
|
||||
// Append all messages in a single atomic transaction. ../rfc/3502:143
|
||||
|
||||
c.account.WithWLock(func() {
|
||||
var changes []store.Change
|
||||
|
||||
c.xdbwrite(func(tx *bstore.Tx) {
|
||||
mb = c.xmailbox(tx, name, "TRYCREATE")
|
||||
|
||||
// Ensure keywords are stored in mailbox.
|
||||
// Check quota for all messages at once.
|
||||
ok, maxSize, err := c.account.CanAddMessageSize(tx, totalSize)
|
||||
xcheckf(err, "checking quota")
|
||||
if !ok {
|
||||
// ../rfc/9208:472
|
||||
xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
|
||||
}
|
||||
|
||||
modseq, err := c.account.NextModSeq(tx)
|
||||
xcheckf(err, "get next mod seq")
|
||||
|
||||
var mbKwChanged bool
|
||||
mb.Keywords, mbKwChanged = store.MergeKeywords(mb.Keywords, keywords)
|
||||
for _, a := range appends {
|
||||
// Ensure keywords are stored in mailbox.
|
||||
var kwch bool
|
||||
mb.Keywords, kwch = store.MergeKeywords(mb.Keywords, a.keywords)
|
||||
mbKwChanged = mbKwChanged || kwch
|
||||
}
|
||||
if mbKwChanged {
|
||||
changes = append(changes, mb.ChangeKeywords())
|
||||
}
|
||||
|
||||
m = store.Message{
|
||||
MailboxID: mb.ID,
|
||||
MailboxOrigID: mb.ID,
|
||||
Received: tm,
|
||||
Flags: storeFlags,
|
||||
Keywords: keywords,
|
||||
Size: mw.Size,
|
||||
for _, a := range appends {
|
||||
a.m = store.Message{
|
||||
MailboxID: mb.ID,
|
||||
MailboxOrigID: mb.ID,
|
||||
Received: a.time,
|
||||
Flags: a.storeFlags,
|
||||
Keywords: a.keywords,
|
||||
Size: a.mw.Size,
|
||||
ModSeq: modseq,
|
||||
CreateSeq: modseq,
|
||||
}
|
||||
mb.Add(a.m.MailboxCounts())
|
||||
}
|
||||
|
||||
ok, maxSize, err := c.account.CanAddMessageSize(tx, m.Size)
|
||||
xcheckf(err, "checking quota")
|
||||
if !ok {
|
||||
// ../rfc/9051:5155 ../rfc/9208:472
|
||||
xusercodeErrorf("OVERQUOTA", "account over maximum total message size %d", maxSize)
|
||||
}
|
||||
|
||||
mb.Add(m.MailboxCounts())
|
||||
|
||||
// Update mailbox before delivering, which updates uidnext which we mustn't overwrite.
|
||||
mb.ModSeq = modseq
|
||||
err = tx.Update(&mb)
|
||||
xcheckf(err, "updating mailbox counts")
|
||||
|
||||
err = c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false, true)
|
||||
xcheckf(err, "delivering message")
|
||||
for _, a := range appends {
|
||||
err = c.account.DeliverMessage(c.log, tx, &a.m, a.file, true, false, false, true)
|
||||
xcheckf(err, "delivering message")
|
||||
|
||||
// Update path to what is stored in the account. We may still have to clean it up on errors.
|
||||
a.path = c.account.MessagePath(a.m.ID)
|
||||
}
|
||||
})
|
||||
|
||||
// Success, make sure messages aren't cleaned up anymore.
|
||||
committed = true
|
||||
|
||||
// Fetch pending changes, possibly with new UIDs, so we can apply them before adding our own new UID.
|
||||
if c.comm != nil {
|
||||
pendingChanges = c.comm.Get()
|
||||
}
|
||||
|
||||
// Broadcast the change to other connections.
|
||||
changes = append(changes, m.ChangeAddUID(), mb.ChangeCounts())
|
||||
for _, a := range appends {
|
||||
changes = append(changes, a.m.ChangeAddUID())
|
||||
}
|
||||
changes = append(changes, mb.ChangeCounts())
|
||||
c.broadcast(changes)
|
||||
})
|
||||
|
||||
if c.mailboxID == mb.ID {
|
||||
c.applyChanges(pendingChanges, false)
|
||||
c.uidAppend(m.UID)
|
||||
for _, a := range appends {
|
||||
c.uidAppend(a.m.UID)
|
||||
}
|
||||
// todo spec: with condstore/qresync, is there a mechanism to the client know the modseq for the appended uid? in theory an untagged fetch with the modseq after the OK APPENDUID could make sense, but this probably isn't allowed.
|
||||
c.bwritelinef("* %d EXISTS", len(c.uids))
|
||||
}
|
||||
|
||||
c.writeresultf("%s OK [APPENDUID %d %d] appended", tag, mb.UIDValidity, m.UID)
|
||||
// ../rfc/4315:289 ../rfc/3502:236 APPENDUID
|
||||
// ../rfc/4315:276 ../rfc/4315:310 UID, and UID set for multiappend
|
||||
var uidset string
|
||||
if len(appends) == 1 {
|
||||
uidset = fmt.Sprintf("%d", appends[0].m.UID)
|
||||
} else {
|
||||
uidset = fmt.Sprintf("%d:%d", appends[0].m.UID, appends[len(appends)-1].m.UID)
|
||||
}
|
||||
c.writeresultf("%s OK [APPENDUID %d %s] appended", tag, mb.UIDValidity, uidset)
|
||||
}
|
||||
|
||||
// Idle makes a client wait until the server sends untagged updates, e.g. about
|
||||
|
|
|
@ -333,6 +333,14 @@ func xparseNumSet(s string) imapclient.NumSet {
|
|||
return ns
|
||||
}
|
||||
|
||||
func xparseUIDRange(s string) imapclient.NumRange {
|
||||
nr, err := imapclient.ParseUIDRange(s)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("parsing uid range %s: %s", s, err))
|
||||
}
|
||||
return nr
|
||||
}
|
||||
|
||||
var connCounter int64
|
||||
|
||||
func start(t *testing.T) *testconn {
|
||||
|
|
|
@ -176,7 +176,7 @@ https://www.iana.org/assignments/message-headers/message-headers.xhtml
|
|||
2683 Yes - IMAP4 Implementation Recommendations
|
||||
2971 Yes - IMAP4 ID extension
|
||||
3348 Yes Obs (RFC 5258) The Internet Message Action Protocol (IMAP4) Child Mailbox Extension
|
||||
3502 Roadmap - Internet Message Access Protocol (IMAP) - MULTIAPPEND Extension
|
||||
3502 Yes - Internet Message Access Protocol (IMAP) - MULTIAPPEND Extension
|
||||
3503 ? - Message Disposition Notification (MDN) profile for Internet Message Access Protocol (IMAP)
|
||||
3516 Yes - IMAP4 Binary Content Extension
|
||||
3691 Yes - Internet Message Access Protocol (IMAP) UNSELECT command
|
||||
|
|
|
@ -1532,10 +1532,10 @@ func (a *Account) WithRLock(fn func()) {
|
|||
//
|
||||
// If the destination mailbox has the Sent special-use flag, the message is parsed
|
||||
// for its recipients (to/cc/bcc). Their domains are added to Recipients for use in
|
||||
// dmarc reputation.
|
||||
// reputation classification.
|
||||
//
|
||||
// If sync is true, the message file and its directory are synced. Should be true
|
||||
// for regular mail delivery, but can be false when importing many messages.
|
||||
// If sync is true, the message file and its directory will be synced. Should be
|
||||
// true for regular mail delivery, but can be false when importing many messages.
|
||||
//
|
||||
// If updateDiskUsage is true, the account total message size (for quota) is
|
||||
// updated. Callers must check if a message can be added within quota before
|
||||
|
|
Loading…
Reference in a new issue