From 6def9f892baf6c31d7a5c55487dc1ab2c943bbea Mon Sep 17 00:00:00 2001 From: arzumify Date: Thu, 20 Mar 2025 18:37:09 +0000 Subject: [PATCH] Testing time --- backend.go | 79 ++++++--- go.mod | 10 +- go.sum | 37 +--- mailbox.go | 358 ++++++++++++++++++++------------------- main.go | 39 ++--- message.go | 486 +++++++++++++++++------------------------------------ session.go | 160 +++++++++++------- user.go | 46 +++-- 8 files changed, 535 insertions(+), 680 deletions(-) diff --git a/backend.go b/backend.go index 98822d8..e694bb8 100644 --- a/backend.go +++ b/backend.go @@ -2,7 +2,10 @@ package main import ( "crypto/ed25519" + "errors" "github.com/emersion/go-imap/v2/imapserver" + "github.com/google/uuid" + "sync" ) // OAuthConfig is the configuration for OAuth. @@ -15,43 +18,65 @@ type OAuthConfig struct { // // A server contains a list of users. type Server struct { - config OAuthConfig + config OAuthConfig + userMutex sync.Mutex + users map[uuid.UUID]*User } // New creates a new server. func New(config OAuthConfig) *Server { return &Server{ config: config, + users: make(map[uuid.UUID]*User), } } +func (s *Server) userRaw(sub uuid.UUID) (*User, error) { + s.userMutex.Lock() + defer s.userMutex.Unlock() + user, ok := s.users[sub] + if !ok { + user = &User{ + mailboxes: make(map[string]*Mailbox), + sessions: make(map[*UserSession]struct{}), + server: s, + sub: sub, + } + + s.users[sub] = user + + _, err := user.mailbox("INBOX") + if err != nil { + if errors.Is(err, ErrNoSuchMailbox) { + err := user.Create("INBOX", nil) + if err != nil { + return nil, err + } + } else { + return nil, err + } + } + } + + return user, nil +} + +func (s *Server) user(token string) (*User, error) { + sub, err := Authenticate(token, s.config) + if err != nil { + return nil, err + } + + return s.userRaw(sub) +} + // NewSession creates a new IMAP session. func (s *Server) NewSession() imapserver.Session { - return &serverSession{ - server: s, - UserSession: &UserSession{ - user: &User{ - server: s, - }, - mailbox: nil, - }, + return s.newSession() +} + +func (s *Server) newSession() *EntryPoint { + return &EntryPoint{ + Server: s, } } - -type serverSession struct { - *UserSession // may be nil - - server *Server // immutable -} - -var _ imapserver.Session = (*serverSession)(nil) - -func (sess *serverSession) SLogin(username, token string) error { - sess.user = &User{server: sess.server} - err := sess.user.Login(username, token) - if err != nil { - return err - } - - return nil -} diff --git a/go.mod b/go.mod index 7b43233..04af02b 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,10 @@ require ( git.ailur.dev/ailur/fg-library/v3 v3.6.2 git.ailur.dev/ailur/fg-nucleus-library v1.2.2 git.ailur.dev/ailur/smtp v1.1.2 - github.com/KEINOS/go-countline v1.1.0 github.com/OneOfOne/xxhash v1.2.8 github.com/emersion/go-imap v1.2.1 - github.com/emersion/go-imap/v2 v2.0.0-beta.4 - github.com/emersion/go-message v0.18.1 + github.com/emersion/go-imap/v2 v2.0.0-beta.5 + github.com/emersion/go-message v0.18.2 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 ) @@ -20,7 +19,6 @@ replace git.ailur.dev/ailur/smtp v1.1.0 => /home/liqing/Projects/libraries/smtp require ( git.ailur.dev/ailur/spf v1.0.1 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/go-chi/chi/v5 v5.2.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - golang.org/x/text v0.21.0 // indirect + github.com/go-chi/chi/v5 v5.2.1 // indirect + golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 250b7e4..bbf96a7 100644 --- a/go.sum +++ b/go.sum @@ -6,44 +6,26 @@ git.ailur.dev/ailur/smtp v1.1.2 h1:CobrkM5VGmobMxXx6r3S9xIeE695dDg5wDiXClf8BCU= git.ailur.dev/ailur/smtp v1.1.2/go.mod h1:M2FqnSK9fo/Dm2m68CTbLaRsH3Q+7MO3TlIx0G/LaSs= git.ailur.dev/ailur/spf v1.0.1 h1:ApkuF2YsQJgUMo0I4cmQcHBERXZ+ZspOkqMe2lyaUfk= git.ailur.dev/ailur/spf v1.0.1/go.mod h1:j+l6sReELJT3VCyAt/DgOfNqNYU/AvzJvj5vgLt3WGo= -github.com/KEINOS/go-countline v1.1.0 h1:D2ECtLPq19NWWN6inXbWhDPhPVN6yGiuf5rrZPLm8kM= -github.com/KEINOS/go-countline v1.1.0/go.mod h1:GNxrrIzaSy97XQijHxa+/3lYagBujtks6jOv9XnJ00k= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= -github.com/emersion/go-imap/v2 v2.0.0-beta.4 h1:BS7+kUVhe/jfuFWgn8li0AbCKBIDoNvqJWsRJppltcc= -github.com/emersion/go-imap/v2 v2.0.0-beta.4/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= +github.com/emersion/go-imap/v2 v2.0.0-beta.5 h1:H3858DNmBuXyMK1++YrQIRdpKE1MwBc+ywBtg3n+0wA= +github.com/emersion/go-imap/v2 v2.0.0-beta.5/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= -github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= -github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= +github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= -github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= -github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w= -github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -68,17 +50,14 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mailbox.go b/mailbox.go index 2ef228f..89d926c 100644 --- a/mailbox.go +++ b/mailbox.go @@ -3,11 +3,12 @@ package main import ( "database/sql" "errors" - "github.com/KEINOS/go-countline/cl" "github.com/google/uuid" "io" "os" "sort" + "strconv" + "sync" "syscall" "time" @@ -16,18 +17,19 @@ import ( "github.com/emersion/go-imap/v2/imapserver" ) -// Mailbox is an in-memory mailbox. +// Mailbox is an IMAP mailbox. // // The same mailbox can be shared between multiple connections and multiple // users. type Mailbox struct { UIDValidity uint32 - openSessions []*MailboxView tracker *imapserver.MailboxTracker Subscribed bool id uuid.UUID name string user *User + sessionMutex sync.Mutex + sessions map[*UserSession]struct{} } var ErrNoSuchMailbox = &imap.Error{ @@ -63,6 +65,7 @@ func loadMboxRaw(subscribed bool, idRaw []byte, name string, u *User) (*Mailbox, name: name, user: u, UIDValidity: xxhash.Checksum32(id[:]), + sessions: make(map[*UserSession]struct{}), } count, err := mbox.getCount() @@ -78,7 +81,7 @@ func loadMboxRaw(subscribed bool, idRaw []byte, name string, u *User) (*Mailbox, func (mbox *Mailbox) Delete() error { view := mbox.NewView() defer func() { - err := view.Close() + err := view.close() if err != nil { log("Failed to close view: "+err.Error()+", resource leaks may occur", 1) } @@ -93,12 +96,14 @@ func (mbox *Mailbox) Delete() error { return err } - for _, view := range mbox.openSessions { - err = view.Close() + mbox.sessionMutex.Lock() + for session := range mbox.sessions { + err = session.Unselect() if err != nil { return err } } + mbox.sessionMutex.Unlock() return nil } @@ -279,91 +284,6 @@ func (mbox *Mailbox) getSize() (int64, error) { return size, nil } -func (mbox *MailboxView) appendLiteral(r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) { - return mbox.appendBuffer(r, r.Size(), options) -} - -func (mbox *MailboxView) copyMsg(msg *message) (*imap.AppendData, error) { - return mbox.appendBuffer(msg.buf, msg.len, &imap.AppendOptions{ - Time: msg.t, - Flags: msg.flagList(), - }) -} - -func (mbox *MailboxView) appendBuffer(buf io.Reader, length int64, options *imap.AppendOptions) (*imap.AppendData, error) { - msg := &message{ - flags: make(map[imap.Flag]struct{}), - len: length, - id: uuid.New(), - } - - mbox.openMessages = append(mbox.openMessages, msg) - - if options.Time.IsZero() { - msg.t = time.Now() - } else { - msg.t = options.Time - } - - for _, flag := range options.Flags { - msg.flags[canonicalFlag(flag)] = struct{}{} - } - - var err error - msg.uid, err = mbox.uidNext() - if err != nil { - return nil, err - } - - err = StoreFile(msg.id.String(), &io.LimitedReader{ - R: buf, - N: length, - }, mbox.user.sub) - if err != nil { - return nil, err - } - - file, err := GetFile(msg.id.String(), mbox.user.sub) - if err != nil { - return nil, err - } - - lines, err := cl.CountLines(file) - if err != nil { - return nil, err - } - msg.lines = int64(lines) - _, err = file.Seek(0, io.SeekStart) - if err != nil { - return nil, err - } - msg.buf = file - - _, err = Database.DB.Exec("INSERT INTO messages (id, owner, uid, created, bodySize, mailbox) VALUES ($1, $2, $3, $4, $5, $6)", msg.id[:], mbox.user.sub[:], msg.uid, msg.t.Unix(), length, mbox.id[:]) - if err != nil { - return nil, err - } - - for flag := range msg.flags { - _, err = Database.DB.Exec("INSERT INTO flags (id, flag) VALUES ($1, $2)", msg.id[:], string(canonicalFlag(flag))) - if err != nil { - return nil, err - } - } - - count, err := mbox.getCount() - if err != nil { - return nil, err - } - - mbox.Mailbox.tracker.QueueNumMessages(count) - - return &imap.AppendData{ - UIDValidity: mbox.Mailbox.UIDValidity, - UID: msg.uid, - }, nil -} - func (mbox *Mailbox) rename(newName string) error { _, err := Database.DB.Exec("UPDATE mailboxes SET mailbox = $1 WHERE id = $2", newName, mbox.id[:]) if err != nil { @@ -436,6 +356,59 @@ func (mbox *Mailbox) getFlags() []imap.Flag { return l } +// A MailboxView is a view into a mailbox. +// +// Each view has its own queue of pending unilateral updates. +// +// Once the mailbox view is no longer used, Close must be called. +// +// Typically, a new MailboxView is created for each IMAP connection in the +// selected state. +type MailboxView struct { + *Mailbox + messageSequenceNum map[imap.UID]uint32 + nextSeqNum uint32 + messageMutex sync.Mutex + openMessages []*message + tracker *imapserver.SessionTracker + searchRes imap.UIDSet +} + +// NewView creates a new view into this mailbox. +// +// Callers must call MailboxView.Close once they are done with the mailbox view. +func (mbox *Mailbox) NewView() *MailboxView { + view := &MailboxView{ + Mailbox: mbox, + tracker: mbox.tracker.NewSession(), + messageSequenceNum: make(map[imap.UID]uint32), + nextSeqNum: 1, + } + + // Index the sequence numbers of messages by UID + messages, err := Database.DB.Query("SELECT uid FROM messages WHERE mailbox = $1", mbox.id[:]) + if err != nil { + return nil + } + + for messages.Next() { + var uid uint32 + err := messages.Scan(&uid) + if err != nil { + return nil + } + view.messageSequenceNum[imap.UID(uid)] = view.nextSeqNum + view.nextSeqNum++ + } + + return view +} + +func (mbox *MailboxView) newUID(uid imap.UID) { + mbox.messageSequenceNum[uid] = mbox.nextSeqNum + mbox.nextSeqNum++ +} + func (mbox *MailboxView) ExpungeAll(w *imapserver.ExpungeWriter) error { messages, err := Database.DB.Query("SELECT uid, id FROM messages WHERE mailbox = $1", mbox.id[:]) if err != nil { @@ -450,6 +423,8 @@ func (mbox *MailboxView) ExpungeAll(w *imapserver.ExpungeWriter) error { return err } + delete(mbox.messageSequenceNum, imap.UID(uid)) + id, err := uuid.FromBytes(idBytes) if err != nil { return err @@ -491,9 +466,11 @@ func (mbox *MailboxView) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) return errors.New("incomplete UID set") } - for uid := range uidSlice { + for _, uid := range uidSlice { + delete(mbox.messageSequenceNum, uid) + var idBytes []byte - err := Database.DB.QueryRow("SELECT id FROM messages WHERE uid = $1", uint32(uid)).Scan(&idBytes) + err := Database.DB.QueryRow("SELECT id FROM messages WHERE uid = $1", uid).Scan(&idBytes) if err != nil { return err } @@ -519,7 +496,7 @@ func (mbox *MailboxView) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) } if w != nil { - err = w.WriteExpunge(mbox.messageSequenceNum[imap.UID(uid)]) + err = w.WriteExpunge(mbox.messageSequenceNum[uid]) if err != nil { return err } @@ -529,55 +506,92 @@ func (mbox *MailboxView) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) return nil } -// NewView creates a new view into this mailbox. -// -// Callers must call MailboxView.Close once they are done with the mailbox view. -func (mbox *Mailbox) NewView() *MailboxView { - view := &MailboxView{ - Mailbox: mbox, - tracker: mbox.tracker.NewSession(), - messageSequenceNum: make(map[imap.UID]uint32), - } - - // Index the sequence numbers of messages by UID - messages, err := Database.DB.Query("SELECT uid FROM messages WHERE mailbox = $1", mbox.id[:]) +func (mbox *MailboxView) copyMsg(msg *message) (*imap.AppendData, error) { + buf, err := msg.buf() if err != nil { - return nil + return nil, err + } + defer buf.Close() + data, err := mbox.appendBuffer(buf, msg.len, &imap.AppendOptions{ + Time: msg.t, + Flags: msg.flagList(), + }) + if err != nil { + return nil, err + } + return data, nil +} + +func (mbox *MailboxView) appendBuffer(buf io.Reader, length int64, options *imap.AppendOptions) (*imap.AppendData, error) { + msg := &message{ + flags: make(map[imap.Flag]struct{}), + len: length, + id: uuid.New(), } - var i uint32 = 1 - for messages.Next() { - var uid uint32 - err := messages.Scan(&uid) + mbox.messageMutex.Lock() + mbox.openMessages = append(mbox.openMessages, msg) + mbox.messageMutex.Unlock() + + if options.Time.IsZero() { + msg.t = time.Now() + } else { + msg.t = options.Time + } + + for _, flag := range options.Flags { + msg.flags[canonicalFlag(flag)] = struct{}{} + } + + var err error + msg.uid, err = mbox.uidNext() + if err != nil { + return nil, err + } + + err = StoreFile(msg.id.String(), &io.LimitedReader{ + R: buf, + N: length, + }, mbox.user.sub) + if err != nil { + return nil, err + } + + msg.underlyingBuf, err = GetFile(msg.id.String(), mbox.user.sub) + if err != nil { + return nil, err + } + + _, err = Database.DB.Exec("INSERT INTO messages (id, owner, uid, created, bodySize, mailbox) VALUES ($1, $2, $3, $4, $5, $6)", msg.id[:], mbox.user.sub[:], msg.uid, msg.t.Unix(), length, mbox.id[:]) + if err != nil { + return nil, err + } + + for flag := range msg.flags { + _, err = Database.DB.Exec("INSERT INTO flags (id, flag) VALUES ($1, $2)", msg.id[:], string(canonicalFlag(flag))) if err != nil { - return nil + return nil, err } - view.messageSequenceNum[imap.UID(uid)] = i - i++ } - mbox.openSessions = append(mbox.openSessions, view) - return view + for session := range mbox.sessions { + session.newUID(msg.uid) + } + + count, err := mbox.getCount() + if err != nil { + return nil, err + } + + mbox.Mailbox.tracker.QueueNumMessages(count) + + return &imap.AppendData{ + UIDValidity: mbox.Mailbox.UIDValidity, + UID: msg.uid, + }, nil } -// A MailboxView is a view into a mailbox. -// -// Each view has its own queue of pending unilateral updates. -// -// Once the mailbox view is no longer used, Close must be called. -// -// Typically, a new MailboxView is created for each IMAP connection in the -// selected state. -type MailboxView struct { - *Mailbox - messageSequenceNum map[imap.UID]uint32 - openMessages []*message - tracker *imapserver.SessionTracker - searchRes imap.UIDSet -} - -// Close releases the resources allocated for the mailbox view. -func (mbox *MailboxView) Close() error { +func (mbox *MailboxView) close() error { defer func() { // Ignore panics, it's probably because the mailbox is already closed recover() @@ -586,7 +600,7 @@ func (mbox *MailboxView) Close() error { mbox.tracker.Close() } for _, msg := range mbox.openMessages { - err := msg.buf.Close() + err := msg.underlyingBuf.Close() if err != nil { if !errors.Is(err, os.ErrClosed) && !errors.Is(err, syscall.EINVAL) { return err @@ -605,24 +619,20 @@ func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, op } } - fetchID := uuid.New() - - var err error - err = mbox.forEach(numSet, func(seqNum uint32, msg *message) { - if err != nil { - return - } - + err := mbox.forEach(numSet, func(seqNum uint32, msg *message) error { if markSeen { msg.flags[canonicalFlag(imap.FlagSeen)] = struct{}{} mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), nil) } respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum)) - err = msg.fetch(respWriter, options) - }) + err := msg.fetch(respWriter, options) + if err != nil { + return err + } - println("FINISHED FETCHING FOR FETCH ID " + fetchID.String()) + return nil + }) return err } @@ -647,7 +657,9 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1) i++ - msg := &message{} + msg := &message{ + flags: make(map[imap.Flag]struct{}), + } var idBytes []byte var timeRaw int64 @@ -663,27 +675,21 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc return nil, err } - file, err := GetFile(msg.id.String(), mbox.user.sub) + msg.underlyingBuf, err = GetFile(msg.id.String(), mbox.user.sub) if err != nil { return nil, err } - lines, err := cl.CountLines(file) + ok, err := msg.search(seqNum, criteria) if err != nil { return nil, err } - msg.lines = int64(lines) - _, err = file.Seek(0, io.SeekStart) - if err != nil { - return nil, err - } - msg.buf = file - - if !msg.search(seqNum, criteria) { + if !ok { continue } // Always populate the UID set, since it may be saved later for SEARCHRES + //goland:noinspection GoDfaNilDereference uidSet.AddNum(msg.uid) var num uint32 @@ -692,6 +698,7 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc if seqNum == 0 { continue } + //goland:noinspection GoDfaNilDereference seqSet.AddNum(seqNum) num = seqNum case imapserver.NumKindUID: @@ -747,10 +754,11 @@ func (mbox *MailboxView) staticSearchCriteria(criteria *imap.SearchCriteria) { } } -func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error { - err := mbox.forEach(numSet, func(seqNum uint32, msg *message) { +func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, _ *imap.StoreOptions) error { + err := mbox.forEach(numSet, func(seqNum uint32, msg *message) error { msg.store(flags) mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker) + return nil }) if err != nil { return err @@ -769,7 +777,7 @@ func (mbox *MailboxView) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) return mbox.tracker.Idle(w, stop) } -func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg *message)) error { +func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg *message) error) error { // TODO: optimize numSet = mbox.staticNumSet(numSet) @@ -780,7 +788,9 @@ func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg * } for messages.Next() { - msg := &message{} + msg := &message{ + flags: make(map[imap.Flag]struct{}), + } var idBytes []byte var timeRaw int64 @@ -791,31 +801,24 @@ func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg * msg.t = time.Unix(timeRaw, 0) - seqNum := mbox.messageSequenceNum[msg.uid] + seqNum, ok := mbox.messageSequenceNum[msg.uid] + if !ok { + log("GURU MEDITATION: The code is messed up and didn't have a proper index for uid "+strconv.Itoa(int(msg.uid))+" in mailbox "+mbox.name, 1) + } msg.id, err = uuid.FromBytes(idBytes) if err != nil { return err } - file, err := GetFile(msg.id.String(), mbox.user.sub) + msg.underlyingBuf, err = GetFile(msg.id.String(), mbox.user.sub) if err != nil { return err } - lines, err := cl.CountLines(file) - if err != nil { - return err - } - msg.lines = int64(lines) - _, err = file.Seek(0, io.SeekStart) - if err != nil { - return err - } - - msg.buf = file - + mbox.messageMutex.Lock() mbox.openMessages = append(mbox.openMessages, msg) + mbox.messageMutex.Unlock() var contains bool switch numSet := numSet.(type) { @@ -829,7 +832,10 @@ func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg * continue } - f(seqNum, msg) + err = f(seqNum, msg) + if err != nil { + return err + } } return nil diff --git a/main.go b/main.go index 3e3955a..3cb0b5b 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "bytes" "errors" - "fmt" "io" "net" "os" @@ -58,7 +57,6 @@ func log(message string, messageType library.MessageCode) { // Authenticate Fake for testing func Authenticate(_ string, _ OAuthConfig) (uuid.UUID, error) { - println("called") return uuid.MustParse("ef585ba0-0ac6-48f8-8ebb-2b6cd97d6a21"), nil } @@ -237,6 +235,7 @@ func setupDatabase(database library.Database) error { var ( Information *library.ServiceInitializationInformation Database library.Database + IMAPServer *Server SMTPDatabaseBackend = smtp.DatabaseBackend{ CheckUser: func(address *smtp.Address) (bool, error) { var prefix, suffix string @@ -259,29 +258,34 @@ var ( return errors.New("503 5.5.1 User not found") } - user := &User{ - sub: uuid.Must(uuid.FromBytes(idRaw)), + session := IMAPServer.newSession() + closeSession := func() { + err = session.Close() + if err != nil { + log("Failed to close session: "+err.Error(), 2) + } } - - mailbox, err := user.mailbox("INBOX") + err = session.loginRaw(uuid.MustParse(string(idRaw))) if err != nil { + go closeSession() return errors.New("503 5.5.1 User not found") } - view := mailbox.NewView() - - _, err = view.appendBuffer(bytes.NewReader(mail.Data), int64(len(mail.Data)), &imap.AppendOptions{ - Time: time.Now(), - }) + _, err = session.Select("INBOX", nil) if err != nil { - err = view.Close() - if err != nil { - log("Failed to close view: "+err.Error()+", resource leaks may occur", 1) - } + go closeSession() return errors.New("421 4.7.0 Error storing message") } - return nil + _, err = session.appendBuffer(bytes.NewReader(mail.Data), int64(len(mail.Data)), &imap.AppendOptions{ + Time: time.Now(), + }) + if err != nil { + go closeSession() + return errors.New("421 4.7.0 Error storing message") + } + + go closeSession() } return nil @@ -319,9 +323,6 @@ func NewSMTPAuthenticationBackend(OAuthRegistration OAuthConfig) smtp.Authentica return nil, errors.New("503 5.5.1 Invalid authentication method: " + initialResponse[0]) } - fmt.Println("Username: " + username) - fmt.Println("Token: " + token) - sub, err := Authenticate(token, OAuthRegistration) if err != nil { return nil, errors.New("421 4.7.0 Invalid credentials") diff --git a/message.go b/message.go index f679aa4..0db98ee 100644 --- a/message.go +++ b/message.go @@ -6,8 +6,8 @@ import ( "fmt" "github.com/google/uuid" "io" - "mime" "strings" + "sync" "time" "github.com/emersion/go-imap/v2" @@ -17,52 +17,103 @@ import ( "github.com/emersion/go-message/textproto" ) +type MessageBuffer struct { + underlyingBuf io.ReadSeekCloser + closed bool + parent *message +} + +func (b *MessageBuffer) Read(p []byte) (n int, err error) { + if b.closed { + return 0, io.ErrClosedPipe + } + return b.underlyingBuf.Read(p) +} + +func (b *MessageBuffer) Seek(offset int64, whence int) (int64, error) { + if b.closed { + return 0, io.ErrClosedPipe + } + return b.underlyingBuf.Seek(offset, whence) +} + +// Close releases the lock on the parent message. It does not close the underlying buffer. +// It is safe to call Close multiple times. +func (b *MessageBuffer) Close() { + if b.closed { + return + } + b.closed = true + b.parent.bufMutex.Unlock() +} + +// Reset resets the buffer to the initial state, once again reading from the beginning of the underlying buffer. +func (b *MessageBuffer) Reset() error { + if b.closed { + return io.ErrClosedPipe + } + _, err := b.underlyingBuf.Seek(0, io.SeekStart) + return err +} + type message struct { // immutable - uid imap.UID - buf io.ReadCloser - len int64 - t time.Time - id uuid.UUID - lines int64 + uid imap.UID + underlyingBuf io.ReadSeekCloser + bufMutex sync.Mutex + len int64 + t time.Time + id uuid.UUID // mutable, protected by Mailbox.mutex flags map[imap.Flag]struct{} } +func (msg *message) buf() (*MessageBuffer, error) { + msg.bufMutex.Lock() + buf := &MessageBuffer{msg.underlyingBuf, false, msg} + err := buf.Reset() + if err != nil { + msg.bufMutex.Unlock() + return nil, err + } + return buf, nil +} + func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error { w.WriteUID(msg.uid) if options.Flags { - fmt.Println("FLAGS: ", msg.flagList()) w.WriteFlags(msg.flagList()) } if options.InternalDate { w.WriteInternalDate(msg.t) } if options.RFC822Size { - fmt.Println("RFC822SIZE: ", msg.len) w.WriteRFC822Size(msg.len) } if options.Envelope { w.WriteEnvelope(msg.envelope()) } - bs := options.BodyStructure - if bs != nil { - structure, err := msg.bodyStructure(bs.Extended) + if options.BodyStructure != nil { + buf, err := msg.buf() if err != nil { - println("BODYSTRUCTURE ERROR: ", err.Error()) return err } - println("BODYSTRUCTURE: ", structure.Disposition().Value) - w.WriteBodyStructure(structure) + w.WriteBodyStructure(imapserver.ExtractBodyStructure(buf)) + buf.Close() } for _, bs := range options.BodySection { - buf := msg.bodySection(bs) - wc := w.WriteBodySection(bs, msg.len) + msgBuf, err := msg.buf() + if err != nil { + return err + } + buf := imapserver.ExtractBodySection(msgBuf, bs) + wc := w.WriteBodySection(bs, int64(len(buf))) _, writeErr := wc.Write(buf) closeErr := wc.Close() + msgBuf.Close() if writeErr != nil { return writeErr } @@ -71,158 +122,49 @@ func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.Fetch } } + for _, bs := range options.BinarySection { + msgBuf, err := msg.buf() + if err != nil { + return err + } + buf := imapserver.ExtractBinarySection(msgBuf, bs) + wc := w.WriteBinarySection(bs, int64(len(buf))) + _, writeErr := wc.Write(buf) + closeErr := wc.Close() + msgBuf.Close() + if writeErr != nil { + return writeErr + } + if closeErr != nil { + return closeErr + } + } + + for _, bss := range options.BinarySectionSize { + buf, err := msg.buf() + if err != nil { + return err + } + n := imapserver.ExtractBinarySectionSize(buf, bss) + w.WriteBinarySectionSize(bss, n) + buf.Close() + } + return w.Close() } func (msg *message) envelope() *imap.Envelope { - br := bufio.NewReader(msg.buf) + buf, err := msg.buf() + if err != nil { + return nil + } + br := bufio.NewReader(buf) header, err := textproto.ReadHeader(br) if err != nil { return nil } - return getEnvelope(header) -} - -func (msg *message) bodyStructure(extended bool) (imap.BodyStructure, error) { - br := bufio.NewReader(msg.buf) - header, err := textproto.ReadHeader(br) - if err != nil { - return nil, err - } - return getBodyStructure(header, br, extended, uint32(msg.len), msg.lines) -} - -func openMessagePart(header textproto.Header, body io.Reader, parentMediaType string) (textproto.Header, io.Reader) { - msgHeader := gomessage.Header{Header: header} - mediaType, _, _ := msgHeader.ContentType() - if !msgHeader.Has("Content-Type") && parentMediaType == "multipart/digest" { - mediaType = "message/rfc822" - } - if mediaType == "message/rfc822" || mediaType == "message/global" { - br := bufio.NewReader(body) - header, _ = textproto.ReadHeader(br) - return header, br - } - return header, body -} - -func (msg *message) bodySection(item *imap.FetchItemBodySection) []byte { - var ( - header textproto.Header - body io.Reader - ) - - br := bufio.NewReader(msg.buf) - header, err := textproto.ReadHeader(br) - if err != nil { - return nil - } - body = br - - // First part of non-multipart message refers to the message itself - msgHeader := gomessage.Header{Header: header} - mediaType, _, _ := msgHeader.ContentType() - partPath := item.Part - if !strings.HasPrefix(mediaType, "multipart/") && len(partPath) > 0 && partPath[0] == 1 { - partPath = partPath[1:] - } - - // Find the requested part using the provided path - var parentMediaType string - for i := 0; i < len(partPath); i++ { - partNum := partPath[i] - - header, body = openMessagePart(header, body, parentMediaType) - - msgHeader := gomessage.Header{Header: header} - mediaType, typeParams, _ := msgHeader.ContentType() - if !strings.HasPrefix(mediaType, "multipart/") { - if partNum != 1 { - return nil - } - continue - } - - mr := textproto.NewMultipartReader(body, typeParams["boundary"]) - found := false - for j := 1; j <= partNum; j++ { - p, err := mr.NextPart() - if err != nil { - return nil - } - - if j == partNum { - parentMediaType = mediaType - header = p.Header - body = p - found = true - break - } - } - if !found { - return nil - } - } - - if len(item.Part) > 0 { - switch item.Specifier { - case imap.PartSpecifierHeader, imap.PartSpecifierText: - header, body = openMessagePart(header, body, parentMediaType) - } - } - - // Filter header fields - if len(item.HeaderFields) > 0 { - keep := make(map[string]struct{}) - for _, k := range item.HeaderFields { - keep[strings.ToLower(k)] = struct{}{} - } - for field := header.Fields(); field.Next(); { - if _, ok := keep[strings.ToLower(field.Key())]; !ok { - field.Del() - } - } - } - for _, k := range item.HeaderFieldsNot { - header.Del(k) - } - - // Write the requested data to a buffer - var buf bytes.Buffer - - writeHeader := true - switch item.Specifier { - case imap.PartSpecifierNone: - writeHeader = len(item.Part) == 0 - case imap.PartSpecifierText: - writeHeader = false - } - if writeHeader { - if err := textproto.WriteHeader(&buf, header); err != nil { - return nil - } - } - - switch item.Specifier { - case imap.PartSpecifierNone, imap.PartSpecifierText: - if _, err := io.Copy(&buf, body); err != nil { - return nil - } - } - - // Extract partial if any - b := buf.Bytes() - if partial := item.Partial; partial != nil { - end := partial.Offset + partial.Size - if partial.Offset > int64(len(b)) { - return nil - } - if end > int64(len(b)) { - end = int64(len(b)) - } - b = b[partial.Offset:end] - } - return b + buf.Close() + return imapserver.ExtractEnvelope(header) } func (msg *message) flagList() []imap.Flag { @@ -251,101 +193,114 @@ func (msg *message) store(store *imap.StoreFlags) { } } -func (msg *message) reader() *gomessage.Entity { - r, _ := gomessage.Read(msg.buf) +func (msg *message) reader() (*gomessage.Entity, error) { + buf, err := msg.buf() + if err != nil { + return nil, err + } + r, _ := gomessage.Read(buf) + buf.Close() if r == nil { r, _ = gomessage.New(gomessage.Header{}, bytes.NewReader(nil)) } - return r + return r, nil } -func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool { +func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) (bool, error) { for _, seqSet := range criteria.SeqNum { if seqNum == 0 || !seqSet.Contains(seqNum) { - println("DOES NOT CONTAIN 1") - return false + return false, nil } } for _, uidSet := range criteria.UID { if !uidSet.Contains(msg.uid) { - println("DOES NOT CONTAIN 2") - return false + return false, nil } } if !matchDate(msg.t, criteria.Since, criteria.Before) { - println("DOES NOT CONTAIN 3") - return false + return false, nil } for _, flag := range criteria.Flag { if _, ok := msg.flags[canonicalFlag(flag)]; !ok { - println("DOES NOT CONTAIN 4") - return false + return false, nil } } for _, flag := range criteria.NotFlag { if _, ok := msg.flags[canonicalFlag(flag)]; ok { - println("DOES NOT CONTAIN 5") - return false + return false, nil } } if criteria.Larger != 0 && msg.len <= criteria.Larger { - println("DOES NOT CONTAIN 6") - return false + return false, nil } if criteria.Smaller != 0 && msg.len >= criteria.Smaller { - println("DOES NOT CONTAIN 7") - return false + return false, nil } - header := mail.Header{Header: msg.reader().Header} + msgReader, err := msg.reader() + if err != nil { + return false, err + } + + header := mail.Header{Header: msgReader.Header} for _, fieldCriteria := range criteria.Header { if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) { - println("DOES NOT CONTAIN 8") - return false + return false, nil } } if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() { t, err := header.Date() if err != nil { - println("DOES NOT CONTAIN 9") - return false + return false, err } else if !matchDate(t, criteria.SentSince, criteria.SentBefore) { - println("DOES NOT CONTAIN 10") - return false + return false, nil } } for _, text := range criteria.Text { - if !matchEntity(msg.reader(), text, true) { - println("DOES NOT CONTAIN 11") - return false + msgReader, err := msg.reader() + if err != nil { + return false, err + } + if !matchEntity(msgReader, text, true) { + return false, nil } } for _, body := range criteria.Body { - if !matchEntity(msg.reader(), body, false) { - println("DOES NOT CONTAIN 12") - return false + msgReader, err := msg.reader() + if err != nil { + return false, err + } + if !matchEntity(msgReader, body, false) { + return false, nil } } for _, not := range criteria.Not { - if msg.search(seqNum, ¬) { - println("DOES NOT CONTAIN 13") - return false + search, err := msg.search(seqNum, ¬) + if err != nil { + return false, err + } + if search { + return false, nil } } for _, or := range criteria.Or { - if !msg.search(seqNum, &or[0]) && !msg.search(seqNum, &or[1]) { - println("DOES NOT CONTAIN 14") - return false + search, err := msg.search(seqNum, &or[0]) + if err != nil { + return false, err + } + search2, err := msg.search(seqNum, &or[1]) + if !search && !search2 { + return false, nil } } - return true + return true, nil } func matchDate(t, since, before time.Time) bool { @@ -420,139 +375,6 @@ func matchEntity(e *gomessage.Entity, pattern string, includeHeader bool) bool { } } -func getEnvelope(h textproto.Header) *imap.Envelope { - mh := mail.Header{Header: gomessage.Header{Header: h}} - date, _ := mh.Date() - inReplyTo, _ := mh.MsgIDList("In-Reply-To") - messageID, _ := mh.MessageID() - return &imap.Envelope{ - Date: date, - Subject: h.Get("Subject"), - From: parseAddressList(mh, "From"), - Sender: parseAddressList(mh, "Sender"), - ReplyTo: parseAddressList(mh, "Reply-To"), - To: parseAddressList(mh, "To"), - Cc: parseAddressList(mh, "Cc"), - Bcc: parseAddressList(mh, "Bcc"), - InReplyTo: inReplyTo, - MessageID: messageID, - } -} - -func parseAddressList(mh mail.Header, k string) []imap.Address { - // TODO: leave the quoted words unchanged - // TODO: handle groups - addrs, _ := mh.AddressList(k) - var l []imap.Address - for _, addr := range addrs { - mailbox, host, ok := strings.Cut(addr.Address, "@") - if !ok { - continue - } - l = append(l, imap.Address{ - Name: mime.QEncoding.Encode("utf-8", addr.Name), - Mailbox: mailbox, - Host: host, - }) - } - return l -} - -func getBodyStructure(rawHeader textproto.Header, r io.Reader, extended bool, length uint32, lines int64) (imap.BodyStructure, error) { - header := gomessage.Header{Header: rawHeader} - - mediaType, typeParams, _ := header.ContentType() - primaryType, subType, _ := strings.Cut(mediaType, "/") - - if primaryType == "multipart" { - bs := &imap.BodyStructureMultiPart{Subtype: subType} - mr := textproto.NewMultipartReader(r, typeParams["boundary"]) - for { - part, _ := mr.NextPart() - if part == nil { - break - } - structure, err := getBodyStructure(part.Header, part, extended, length, lines) - if err != nil { - return nil, err - } - bs.Children = append(bs.Children, structure) - } - if extended { - bs.Extended = &imap.BodyStructureMultiPartExt{ - Params: typeParams, - Disposition: getContentDisposition(header), - Language: getContentLanguage(header), - Location: header.Get("Content-Location"), - } - } - return bs, nil - } else { - bs := &imap.BodyStructureSinglePart{ - Type: primaryType, - Subtype: subType, - Params: typeParams, - ID: header.Get("Content-Id"), - Description: header.Get("Content-Description"), - Encoding: header.Get("Content-Transfer-Encoding"), - Size: length, - } - if mediaType == "message/rfc822" || mediaType == "message/global" { - br := bufio.NewReader(r) - childHeader, err := textproto.ReadHeader(br) - if err != nil { - return nil, err - } - structure, err := getBodyStructure(childHeader, br, extended, length, lines) - if err != nil { - return nil, err - } - bs.MessageRFC822 = &imap.BodyStructureMessageRFC822{ - Envelope: getEnvelope(childHeader), - BodyStructure: structure, - NumLines: lines, - } - } - if primaryType == "text" { - bs.Text = &imap.BodyStructureText{ - NumLines: lines, - } - } - if extended { - bs.Extended = &imap.BodyStructureSinglePartExt{ - Disposition: getContentDisposition(header), - Language: getContentLanguage(header), - Location: header.Get("Content-Location"), - } - } - return bs, nil - } -} - -func getContentDisposition(header gomessage.Header) *imap.BodyStructureDisposition { - disp, dispParams, _ := header.ContentDisposition() - if disp == "" { - return nil - } - return &imap.BodyStructureDisposition{ - Value: disp, - Params: dispParams, - } -} - -func getContentLanguage(header gomessage.Header) []string { - v := header.Get("Content-Language") - if v == "" { - return nil - } - // TODO: handle CFWS - l := strings.Split(v, ",") - for i, lang := range l { - l[i] = strings.TrimSpace(lang) - } - return l -} - func canonicalFlag(flag imap.Flag) imap.Flag { return imap.Flag(strings.ToLower(string(flag))) } diff --git a/session.go b/session.go index dc2e8e4..bd98e6c 100644 --- a/session.go +++ b/session.go @@ -3,12 +3,7 @@ package main import ( "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapserver" - "io" -) - -type ( - user = User - mailbox = MailboxView + "github.com/google/uuid" ) // UserSession represents a session tied to a specific user. @@ -16,76 +11,116 @@ type ( // UserSession implements imapserver.Session. Typically, a UserSession pointer // is embedded into a larger struct which overrides Login. type UserSession struct { - *user // immutable - *mailbox // may be nil + // immutable + *User + *EntryPoint + + // may be nil + *MailboxView } -var _ imapserver.SessionIMAP4rev2 = (*UserSession)(nil) +var _ imapserver.SessionIMAP4rev2 = (*EntryPoint)(nil) +var _ imapserver.Session = (*EntryPoint)(nil) -// NewUserSession creates a new user session. -func NewUserSession(user *User) *UserSession { - return &UserSession{user: user} +type EntryPoint struct { + *UserSession + *Server } -type ByteLiteral struct { - io.Reader - size int64 +func (sess *EntryPoint) Login(_, token string) (err error) { + sess.UserSession = &UserSession{ + EntryPoint: sess, + } + sess.UserSession.User, err = sess.Server.user(token) + sess.UserSession.User.sessionMutex.Lock() + sess.UserSession.User.sessions[sess.UserSession] = struct{}{} + sess.UserSession.User.sessionMutex.Unlock() + return err } -func (l *ByteLiteral) Size() int64 { - return l.size +func (sess *EntryPoint) loginRaw(sub uuid.UUID) (err error) { + sess.UserSession = &UserSession{ + EntryPoint: sess, + } + sess.UserSession.User, err = sess.Server.userRaw(sub) + return err } func (sess *UserSession) Append(mboxName string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) { - mbox, err := sess.user.mailbox(mboxName) + mbox, err := sess.User.mailbox(mboxName) if err != nil { return nil, err } view := mbox.NewView() defer func() { - err := view.Close() + err := view.close() if err != nil { log("Failed to close view: "+err.Error()+", resource leaks may occur", 1) } }() - println("INITIAL APPEND ENTRYPOINT SUCCESS") - return view.appendLiteral(&ByteLiteral{Reader: r, size: r.Size()}, options) + return view.appendBuffer(r, r.Size(), options) } func (sess *UserSession) Close() error { - if sess != nil && sess.mailbox != nil { - return sess.mailbox.Close() + if sess == nil || sess.MailboxView == nil { + return nil + } + if sess.User != nil { + err := sess.Unselect() + if err != nil { + return err + } + + sess.User.sessionMutex.Lock() + delete(sess.User.sessions, sess) + if len(sess.User.sessions) == 0 { + sess.Server.userMutex.Lock() + delete(sess.server.users, sess.User.sub) + sess.Server.userMutex.Unlock() + } + sess.User.sessionMutex.Unlock() } return nil } func (sess *UserSession) Select(name string, _ *imap.SelectOptions) (*imap.SelectData, error) { - mbox, err := sess.user.mailbox(name) + mbox, err := sess.User.mailbox(name) if err != nil { return nil, err } - sess.mailbox = mbox.NewView() + mbox.sessionMutex.Lock() + mbox.sessions[sess] = struct{}{} + mbox.sessionMutex.Unlock() + sess.MailboxView = mbox.NewView() return mbox.selectData() } func (sess *UserSession) Unselect() error { - err := sess.mailbox.Close() + sess.Mailbox.sessionMutex.Lock() + defer sess.Mailbox.sessionMutex.Unlock() + delete(sess.Mailbox.sessions, sess) + if len(sess.Mailbox.sessions) == 0 { + sess.User.sessionMutex.Lock() + delete(sess.mailboxes, sess.name) + sess.User.sessionMutex.Unlock() + } + err := sess.close() if err != nil { return err } - sess.mailbox = nil + sess.MailboxView = nil return nil } -func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (data *imap.CopyData, fErr error) { - dest, err := sess.user.mailbox(destName) +func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (*imap.CopyData, error) { + dest, err := sess.mailbox(destName) if err != nil { return nil, &imap.Error{ Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeTryCreate, Text: "No such mailbox", } - } else if sess.mailbox != nil && dest == sess.mailbox.Mailbox { + } else if sess.MailboxView != nil && dest == sess.Mailbox { return nil, &imap.Error{ Type: imap.StatusResponseTypeNo, Text: "Source and destination mailboxes are identical", @@ -94,28 +129,26 @@ func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (data *imap.C destView := dest.NewView() defer func() { - err := destView.Close() + err := destView.close() if err != nil { log("Failed to close view: "+err.Error()+", resource leaks may occur", 1) } }() var sourceUIDs, destUIDs imap.UIDSet - err = sess.mailbox.forEach(numSet, func(seqNum uint32, msg *message) { - appendData, err := destView.copyMsg(msg) + err = sess.forEach(numSet, func(seqNum uint32, msg *message) error { + var appendData *imap.AppendData + appendData, err = destView.copyMsg(msg) if err != nil { - fErr = err - return + return err } sourceUIDs.AddNum(msg.uid) destUIDs.AddNum(appendData.UID) + return nil }) if err != nil { return nil, err } - if fErr != nil { - return nil, fErr - } return &imap.CopyData{ UIDValidity: dest.UIDValidity, @@ -124,15 +157,15 @@ func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (data *imap.C }, nil } -func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) (fErr error) { - dest, err := sess.user.mailbox(destName) +func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) error { + dest, err := sess.mailbox(destName) if err != nil { return &imap.Error{ Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeTryCreate, Text: "No such mailbox", } - } else if sess.mailbox != nil && dest == sess.mailbox.Mailbox { + } else if sess.MailboxView != nil && dest == sess.Mailbox { return &imap.Error{ Type: imap.StatusResponseTypeNo, Text: "Source and destination mailboxes are identical", @@ -141,40 +174,39 @@ func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, dest destView := dest.NewView() defer func() { - err := destView.Close() + err := destView.close() if err != nil { log("Failed to close view: "+err.Error()+", resource leaks may occur", 1) } }() var sourceUIDs, destUIDs imap.UIDSet - err = sess.mailbox.forEach(numSet, func(seqNum uint32, msg *message) { + err = sess.forEach(numSet, func(seqNum uint32, msg *message) error { var appendData *imap.AppendData - appendData, fErr = destView.copyMsg(msg) - if fErr != nil { - return + appendData, err = destView.copyMsg(msg) + if err != nil { + return err } sourceUIDs.AddNum(msg.uid) destUIDs.AddNum(appendData.UID) - }) - if err != nil { - return err - } - if fErr != nil { - return fErr - } - - err = w.WriteCopyData(&imap.CopyData{ - UIDValidity: dest.UIDValidity, - SourceUIDs: sourceUIDs, - DestUIDs: destUIDs, + return nil }) if err != nil { return err } - println(sourceUIDs.String()) - err = sess.mailbox.Expunge(nil, &sourceUIDs) + if len(sourceUIDs) != 0 && len(destUIDs) != 0 { + err = w.WriteCopyData(&imap.CopyData{ + UIDValidity: dest.UIDValidity, + SourceUIDs: sourceUIDs, + DestUIDs: destUIDs, + }) + if err != nil { + return err + } + } + + err = sess.Expunge((*imapserver.ExpungeWriter)(w), &sourceUIDs) if err != nil { return err } @@ -183,15 +215,15 @@ func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, dest } func (sess *UserSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error { - if sess.mailbox == nil { + if sess.MailboxView == nil { return nil } - return sess.mailbox.Poll(w, allowExpunge) + return sess.MailboxView.Poll(w, allowExpunge) } func (sess *UserSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error { - if sess.mailbox == nil { + if sess.MailboxView == nil { return nil // TODO } - return sess.mailbox.Idle(w, stop) + return sess.MailboxView.Idle(w, stop) } diff --git a/user.go b/user.go index d0a31ab..6718416 100644 --- a/user.go +++ b/user.go @@ -1,46 +1,38 @@ package main import ( - "errors" "github.com/emersion/go-imap/v2" "github.com/emersion/go-imap/v2/imapserver" "github.com/google/uuid" "sort" "strings" + "sync" ) const mailboxDelim rune = '/' type User struct { - server *Server // may be nil if sub is not set - sub uuid.UUID -} - -func (u *User) Login(_, token string) error { - sub, err := Authenticate(token, u.server.config) - if err != nil { - return err - } - - u.sub = sub - - _, err = u.mailbox("INBOX") - if err != nil { - if errors.Is(err, ErrNoSuchMailbox) { - err := u.Create("INBOX", nil) - if err != nil { - return err - } - } else { - return err - } - } - - return nil + server *Server // may be nil if sub is not set + mailboxMutex sync.Mutex + mailboxes map[string]*Mailbox + sessionMutex sync.Mutex + sessions map[*UserSession]struct{} + sub uuid.UUID } func (u *User) mailbox(name string) (*Mailbox, error) { - return loadMbox(name, u) + u.mailboxMutex.Lock() + defer u.mailboxMutex.Unlock() + mbox, ok := u.mailboxes[name] + if !ok { + var err error + mbox, err = loadMbox(name, u) + if err != nil { + return nil, err + } + u.mailboxes[name] = mbox + } + return mbox, nil } func (u *User) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) {