Support for IMAP MOVE

This commit is contained in:
Neil Alexander
2025-12-02 19:23:49 +00:00
parent d2093e00bf
commit 8385ac78da
6 changed files with 46 additions and 6 deletions

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.24.0
require ( require (
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
github.com/emersion/go-imap-idle v0.0.0-20210907174914-db2568431445 github.com/emersion/go-imap-idle v0.0.0-20210907174914-db2568431445
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872
github.com/emersion/go-message v0.17.0 github.com/emersion/go-message v0.17.0
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/emersion/go-smtp v0.15.0 github.com/emersion/go-smtp v0.15.0

2
go.sum
View File

@@ -17,6 +17,8 @@ github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjT
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-imap-idle v0.0.0-20210907174914-db2568431445 h1:dAGbaaU4LLupO7dnYZaELOoI3RoVDNi5DCGejLe8a7c= github.com/emersion/go-imap-idle v0.0.0-20210907174914-db2568431445 h1:dAGbaaU4LLupO7dnYZaELOoI3RoVDNi5DCGejLe8a7c=
github.com/emersion/go-imap-idle v0.0.0-20210907174914-db2568431445/go.mod h1:N/6S3dRTVt8xT867m+476C16+v/Fq4WZYvh2Chg0nmg= github.com/emersion/go-imap-idle v0.0.0-20210907174914-db2568431445/go.mod h1:N/6S3dRTVt8xT867m+476C16+v/Fq4WZYvh2Chg0nmg=
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 h1:HGBfonz0q/zq7y3ew+4oy4emHSvk6bkmV0mdDG3E77M=
github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04= github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04=

View File

@@ -12,6 +12,7 @@ import (
"log" "log"
idle "github.com/emersion/go-imap-idle" idle "github.com/emersion/go-imap-idle"
move "github.com/emersion/go-imap-move"
"github.com/emersion/go-imap/server" "github.com/emersion/go-imap/server"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
) )
@@ -19,6 +20,7 @@ import (
type IMAPServer struct { type IMAPServer struct {
server *server.Server server *server.Server
backend *Backend backend *Backend
notify *IMAPNotify
} }
func NewIMAPServer(backend *Backend, addr string, insecure bool) (*IMAPServer, *IMAPNotify, error) { func NewIMAPServer(backend *Backend, addr string, insecure bool) (*IMAPServer, *IMAPNotify, error) {
@@ -26,12 +28,13 @@ func NewIMAPServer(backend *Backend, addr string, insecure bool) (*IMAPServer, *
server: server.New(backend), server: server.New(backend),
backend: backend, backend: backend,
} }
notify := NewIMAPNotify(s.server, backend.Log) s.notify = NewIMAPNotify(s.server, backend.Log)
s.server.Addr = addr s.server.Addr = addr
s.server.AllowInsecureAuth = insecure s.server.AllowInsecureAuth = insecure
//s.server.Debug = os.Stdout //s.server.Debug = os.Stdout
s.server.Enable(idle.NewExtension()) s.server.Enable(idle.NewExtension())
//s.server.Enable(notify) s.server.Enable(move.NewExtension())
// s.server.Enable(s.notify)
s.server.EnableAuth(sasl.Login, func(conn server.Conn) sasl.Server { s.server.EnableAuth(sasl.Login, func(conn server.Conn) sasl.Server {
return sasl.NewLoginServer(func(username, password string) error { return sasl.NewLoginServer(func(username, password string) error {
_, err := s.backend.Login(nil, username, password) _, err := s.backend.Login(nil, username, password)
@@ -43,5 +46,5 @@ func NewIMAPServer(backend *Backend, addr string, insecure bool) (*IMAPServer, *
log.Fatal(err) log.Fatal(err)
} }
}() }()
return s, notify, nil return s, s.notify, nil
} }

View File

@@ -13,7 +13,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"time" "time"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
@@ -214,7 +213,7 @@ func (mbox *Mailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([]
} }
func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { func (mbox *Mailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error {
b, err := ioutil.ReadAll(body) b, err := io.ReadAll(body)
if err != nil { if err != nil {
return fmt.Errorf("b.ReadFrom: %w", err) return fmt.Errorf("b.ReadFrom: %w", err)
} }
@@ -312,3 +311,21 @@ func (mbox *Mailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, destName string
func (mbox *Mailbox) Expunge() error { func (mbox *Mailbox) Expunge() error {
return mbox.backend.Storage.MailExpunge(mbox.name) return mbox.backend.Storage.MailExpunge(mbox.name)
} }
func (mbox *Mailbox) MoveMessages(uid bool, seqset *imap.SeqSet, dest string) error {
if dest == "Outbox" {
return fmt.Errorf("can't copy into Outbox as it is a protected folder")
}
ids, err := mbox.getIDsFromSeqSet(uid, seqset)
if err != nil {
return fmt.Errorf("mbox.getIDsFromSeqSet: %w", err)
}
for _, id := range ids {
if err := mbox.backend.Storage.MailMove(mbox.name, int(id), dest); err != nil {
return err
}
}
return nil
}

View File

@@ -30,6 +30,7 @@ type TableMails struct {
updateMailFlags *sql.Stmt updateMailFlags *sql.Stmt
deleteMail *sql.Stmt deleteMail *sql.Stmt
expungeMail *sql.Stmt expungeMail *sql.Stmt
moveMail *sql.Stmt
} }
const mailsSchema = ` const mailsSchema = `
@@ -94,7 +95,7 @@ const selectIDForSeqStmt = `
const selectMailNextID = ` const selectMailNextID = `
SELECT IFNULL(MAX(id)+1,1) AS id FROM mails SELECT IFNULL(MAX(id)+1,1) AS id FROM mails
WHERE mailbox = $1 WHERE mailbox = $1
` `
const updateMailFlagsStmt = ` const updateMailFlagsStmt = `
@@ -109,6 +110,10 @@ const expungeMailStmt = `
DELETE FROM mails WHERE mailbox = $1 AND deleted = 1 DELETE FROM mails WHERE mailbox = $1 AND deleted = 1
` `
const moveMailStmt = `
UPDATE mails SET mailbox = $1 WHERE mailbox = $2 AND id = $3
`
func NewTableMails(db *sql.DB, writer *Writer) (*TableMails, error) { func NewTableMails(db *sql.DB, writer *Writer) (*TableMails, error) {
t := &TableMails{ t := &TableMails{
db: db, db: db,
@@ -162,6 +167,10 @@ func NewTableMails(db *sql.DB, writer *Writer) (*TableMails, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailUnseenStmt): %w", err) return nil, fmt.Errorf("db.Prepare(selectMailUnseenStmt): %w", err)
} }
t.moveMail, err = db.Prepare(moveMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(moveMailStmt): %w", err)
}
return t, nil return t, nil
} }
@@ -246,3 +255,10 @@ func (t *TableMails) MailCount(mailbox string) (int, error) {
err := t.countMails.QueryRow(mailbox).Scan(&count) err := t.countMails.QueryRow(mailbox).Scan(&count)
return count, err return count, err
} }
func (t *TableMails) MailMove(mailbox string, id int, destination string) error {
return t.writer.Do(t.db, nil, func(txn *sql.Tx) error {
_, err := t.moveMail.Exec(destination, mailbox, id)
return err
})
}

View File

@@ -33,6 +33,7 @@ type Storage interface {
MailDelete(mailbox string, id int) error MailDelete(mailbox string, id int) error
MailExpunge(mailbox string) error MailExpunge(mailbox string) error
MailCount(mailbox string) (int, error) MailCount(mailbox string) (int, error)
MailMove(mailbox string, id int, destination string) error
QueueListDestinations() ([]string, error) QueueListDestinations() ([]string, error)
QueueMailIDsForDestination(destination string) ([]types.QueuedMail, error) QueueMailIDsForDestination(destination string) ([]types.QueuedMail, error)