Testing time
This commit is contained in:
parent
b80c3c2fa3
commit
6def9f892b
79
backend.go
79
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
|
||||
}
|
||||
|
|
10
go.mod
10
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
|
||||
)
|
||||
|
|
37
go.sum
37
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=
|
||||
|
|
358
mailbox.go
358
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
|
||||
|
|
39
main.go
39
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")
|
||||
|
|
486
message.go
486
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)))
|
||||
}
|
||||
|
|
160
session.go
160
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)
|
||||
}
|
||||
|
|
46
user.go
46
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) {
|
||||
|
|
Loading…
Reference in New Issue