Testing time
This commit is contained in:
parent
b80c3c2fa3
commit
6def9f892b
79
backend.go
79
backend.go
|
@ -2,7 +2,10 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"errors"
|
||||||
"github.com/emersion/go-imap/v2/imapserver"
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OAuthConfig is the configuration for OAuth.
|
// OAuthConfig is the configuration for OAuth.
|
||||||
|
@ -15,43 +18,65 @@ type OAuthConfig struct {
|
||||||
//
|
//
|
||||||
// A server contains a list of users.
|
// A server contains a list of users.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
config OAuthConfig
|
config OAuthConfig
|
||||||
|
userMutex sync.Mutex
|
||||||
|
users map[uuid.UUID]*User
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new server.
|
// New creates a new server.
|
||||||
func New(config OAuthConfig) *Server {
|
func New(config OAuthConfig) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
config: config,
|
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.
|
// NewSession creates a new IMAP session.
|
||||||
func (s *Server) NewSession() imapserver.Session {
|
func (s *Server) NewSession() imapserver.Session {
|
||||||
return &serverSession{
|
return s.newSession()
|
||||||
server: s,
|
}
|
||||||
UserSession: &UserSession{
|
|
||||||
user: &User{
|
func (s *Server) newSession() *EntryPoint {
|
||||||
server: s,
|
return &EntryPoint{
|
||||||
},
|
Server: s,
|
||||||
mailbox: nil,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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-library/v3 v3.6.2
|
||||||
git.ailur.dev/ailur/fg-nucleus-library v1.2.2
|
git.ailur.dev/ailur/fg-nucleus-library v1.2.2
|
||||||
git.ailur.dev/ailur/smtp v1.1.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/OneOfOne/xxhash v1.2.8
|
||||||
github.com/emersion/go-imap v1.2.1
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/emersion/go-imap/v2 v2.0.0-beta.4
|
github.com/emersion/go-imap/v2 v2.0.0-beta.5
|
||||||
github.com/emersion/go-message v0.18.1
|
github.com/emersion/go-message v0.18.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
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 (
|
require (
|
||||||
git.ailur.dev/ailur/spf v1.0.1 // indirect
|
git.ailur.dev/ailur/spf v1.0.1 // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.0 // indirect
|
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
golang.org/x/text v0.21.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/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 h1:ApkuF2YsQJgUMo0I4cmQcHBERXZ+ZspOkqMe2lyaUfk=
|
||||||
git.ailur.dev/ailur/spf v1.0.1/go.mod h1:j+l6sReELJT3VCyAt/DgOfNqNYU/AvzJvj5vgLt3WGo=
|
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 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
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 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||||
github.com/emersion/go-imap/v2 v2.0.0-beta.4 h1:BS7+kUVhe/jfuFWgn8li0AbCKBIDoNvqJWsRJppltcc=
|
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.4/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
|
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.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.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||||
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
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-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 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
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/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.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
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 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/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-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/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=
|
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.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.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.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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
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=
|
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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/KEINOS/go-countline/cl"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -16,18 +17,19 @@ import (
|
||||||
"github.com/emersion/go-imap/v2/imapserver"
|
"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
|
// The same mailbox can be shared between multiple connections and multiple
|
||||||
// users.
|
// users.
|
||||||
type Mailbox struct {
|
type Mailbox struct {
|
||||||
UIDValidity uint32
|
UIDValidity uint32
|
||||||
openSessions []*MailboxView
|
|
||||||
tracker *imapserver.MailboxTracker
|
tracker *imapserver.MailboxTracker
|
||||||
Subscribed bool
|
Subscribed bool
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
name string
|
name string
|
||||||
user *User
|
user *User
|
||||||
|
sessionMutex sync.Mutex
|
||||||
|
sessions map[*UserSession]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNoSuchMailbox = &imap.Error{
|
var ErrNoSuchMailbox = &imap.Error{
|
||||||
|
@ -63,6 +65,7 @@ func loadMboxRaw(subscribed bool, idRaw []byte, name string, u *User) (*Mailbox,
|
||||||
name: name,
|
name: name,
|
||||||
user: u,
|
user: u,
|
||||||
UIDValidity: xxhash.Checksum32(id[:]),
|
UIDValidity: xxhash.Checksum32(id[:]),
|
||||||
|
sessions: make(map[*UserSession]struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := mbox.getCount()
|
count, err := mbox.getCount()
|
||||||
|
@ -78,7 +81,7 @@ func loadMboxRaw(subscribed bool, idRaw []byte, name string, u *User) (*Mailbox,
|
||||||
func (mbox *Mailbox) Delete() error {
|
func (mbox *Mailbox) Delete() error {
|
||||||
view := mbox.NewView()
|
view := mbox.NewView()
|
||||||
defer func() {
|
defer func() {
|
||||||
err := view.Close()
|
err := view.close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
||||||
}
|
}
|
||||||
|
@ -93,12 +96,14 @@ func (mbox *Mailbox) Delete() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, view := range mbox.openSessions {
|
mbox.sessionMutex.Lock()
|
||||||
err = view.Close()
|
for session := range mbox.sessions {
|
||||||
|
err = session.Unselect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mbox.sessionMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -279,91 +284,6 @@ func (mbox *Mailbox) getSize() (int64, error) {
|
||||||
return size, nil
|
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 {
|
func (mbox *Mailbox) rename(newName string) error {
|
||||||
_, err := Database.DB.Exec("UPDATE mailboxes SET mailbox = $1 WHERE id = $2", newName, mbox.id[:])
|
_, err := Database.DB.Exec("UPDATE mailboxes SET mailbox = $1 WHERE id = $2", newName, mbox.id[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -436,6 +356,59 @@ func (mbox *Mailbox) getFlags() []imap.Flag {
|
||||||
return l
|
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 {
|
func (mbox *MailboxView) ExpungeAll(w *imapserver.ExpungeWriter) error {
|
||||||
messages, err := Database.DB.Query("SELECT uid, id FROM messages WHERE mailbox = $1", mbox.id[:])
|
messages, err := Database.DB.Query("SELECT uid, id FROM messages WHERE mailbox = $1", mbox.id[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -450,6 +423,8 @@ func (mbox *MailboxView) ExpungeAll(w *imapserver.ExpungeWriter) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete(mbox.messageSequenceNum, imap.UID(uid))
|
||||||
|
|
||||||
id, err := uuid.FromBytes(idBytes)
|
id, err := uuid.FromBytes(idBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -491,9 +466,11 @@ func (mbox *MailboxView) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet)
|
||||||
return errors.New("incomplete UID set")
|
return errors.New("incomplete UID set")
|
||||||
}
|
}
|
||||||
|
|
||||||
for uid := range uidSlice {
|
for _, uid := range uidSlice {
|
||||||
|
delete(mbox.messageSequenceNum, uid)
|
||||||
|
|
||||||
var idBytes []byte
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -519,7 +496,7 @@ func (mbox *MailboxView) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
if w != nil {
|
if w != nil {
|
||||||
err = w.WriteExpunge(mbox.messageSequenceNum[imap.UID(uid)])
|
err = w.WriteExpunge(mbox.messageSequenceNum[uid])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -529,55 +506,92 @@ func (mbox *MailboxView) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewView creates a new view into this mailbox.
|
func (mbox *MailboxView) copyMsg(msg *message) (*imap.AppendData, error) {
|
||||||
//
|
buf, err := msg.buf()
|
||||||
// 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[:])
|
|
||||||
if err != nil {
|
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
|
mbox.messageMutex.Lock()
|
||||||
for messages.Next() {
|
mbox.openMessages = append(mbox.openMessages, msg)
|
||||||
var uid uint32
|
mbox.messageMutex.Unlock()
|
||||||
err := messages.Scan(&uid)
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
view.messageSequenceNum[imap.UID(uid)] = i
|
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mbox.openSessions = append(mbox.openSessions, view)
|
for session := range mbox.sessions {
|
||||||
return view
|
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.
|
func (mbox *MailboxView) close() error {
|
||||||
//
|
|
||||||
// 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 {
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// Ignore panics, it's probably because the mailbox is already closed
|
// Ignore panics, it's probably because the mailbox is already closed
|
||||||
recover()
|
recover()
|
||||||
|
@ -586,7 +600,7 @@ func (mbox *MailboxView) Close() error {
|
||||||
mbox.tracker.Close()
|
mbox.tracker.Close()
|
||||||
}
|
}
|
||||||
for _, msg := range mbox.openMessages {
|
for _, msg := range mbox.openMessages {
|
||||||
err := msg.buf.Close()
|
err := msg.underlyingBuf.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, os.ErrClosed) && !errors.Is(err, syscall.EINVAL) {
|
if !errors.Is(err, os.ErrClosed) && !errors.Is(err, syscall.EINVAL) {
|
||||||
return err
|
return err
|
||||||
|
@ -605,24 +619,20 @@ func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchID := uuid.New()
|
err := mbox.forEach(numSet, func(seqNum uint32, msg *message) error {
|
||||||
|
|
||||||
var err error
|
|
||||||
err = mbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if markSeen {
|
if markSeen {
|
||||||
msg.flags[canonicalFlag(imap.FlagSeen)] = struct{}{}
|
msg.flags[canonicalFlag(imap.FlagSeen)] = struct{}{}
|
||||||
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), nil)
|
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum))
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
@ -647,7 +657,9 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc
|
||||||
seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1)
|
seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1)
|
||||||
i++
|
i++
|
||||||
|
|
||||||
msg := &message{}
|
msg := &message{
|
||||||
|
flags: make(map[imap.Flag]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
var idBytes []byte
|
var idBytes []byte
|
||||||
var timeRaw int64
|
var timeRaw int64
|
||||||
|
@ -663,27 +675,21 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines, err := cl.CountLines(file)
|
ok, err := msg.search(seqNum, criteria)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
msg.lines = int64(lines)
|
if !ok {
|
||||||
_, err = file.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msg.buf = file
|
|
||||||
|
|
||||||
if !msg.search(seqNum, criteria) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always populate the UID set, since it may be saved later for SEARCHRES
|
// Always populate the UID set, since it may be saved later for SEARCHRES
|
||||||
|
//goland:noinspection GoDfaNilDereference
|
||||||
uidSet.AddNum(msg.uid)
|
uidSet.AddNum(msg.uid)
|
||||||
|
|
||||||
var num uint32
|
var num uint32
|
||||||
|
@ -692,6 +698,7 @@ func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.Searc
|
||||||
if seqNum == 0 {
|
if seqNum == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
//goland:noinspection GoDfaNilDereference
|
||||||
seqSet.AddNum(seqNum)
|
seqSet.AddNum(seqNum)
|
||||||
num = seqNum
|
num = seqNum
|
||||||
case imapserver.NumKindUID:
|
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 {
|
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) {
|
err := mbox.forEach(numSet, func(seqNum uint32, msg *message) error {
|
||||||
msg.store(flags)
|
msg.store(flags)
|
||||||
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker)
|
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -769,7 +777,7 @@ func (mbox *MailboxView) Idle(w *imapserver.UpdateWriter, stop <-chan struct{})
|
||||||
return mbox.tracker.Idle(w, stop)
|
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
|
// TODO: optimize
|
||||||
|
|
||||||
numSet = mbox.staticNumSet(numSet)
|
numSet = mbox.staticNumSet(numSet)
|
||||||
|
@ -780,7 +788,9 @@ func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg *
|
||||||
}
|
}
|
||||||
|
|
||||||
for messages.Next() {
|
for messages.Next() {
|
||||||
msg := &message{}
|
msg := &message{
|
||||||
|
flags: make(map[imap.Flag]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
var idBytes []byte
|
var idBytes []byte
|
||||||
var timeRaw int64
|
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)
|
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)
|
msg.id, err = uuid.FromBytes(idBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := GetFile(msg.id.String(), mbox.user.sub)
|
msg.underlyingBuf, err = GetFile(msg.id.String(), mbox.user.sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines, err := cl.CountLines(file)
|
mbox.messageMutex.Lock()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
msg.lines = int64(lines)
|
|
||||||
_, err = file.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.buf = file
|
|
||||||
|
|
||||||
mbox.openMessages = append(mbox.openMessages, msg)
|
mbox.openMessages = append(mbox.openMessages, msg)
|
||||||
|
mbox.messageMutex.Unlock()
|
||||||
|
|
||||||
var contains bool
|
var contains bool
|
||||||
switch numSet := numSet.(type) {
|
switch numSet := numSet.(type) {
|
||||||
|
@ -829,7 +832,10 @@ func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg *
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
f(seqNum, msg)
|
err = f(seqNum, msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
39
main.go
39
main.go
|
@ -9,7 +9,6 @@ import (
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
@ -58,7 +57,6 @@ func log(message string, messageType library.MessageCode) {
|
||||||
|
|
||||||
// Authenticate Fake for testing
|
// Authenticate Fake for testing
|
||||||
func Authenticate(_ string, _ OAuthConfig) (uuid.UUID, error) {
|
func Authenticate(_ string, _ OAuthConfig) (uuid.UUID, error) {
|
||||||
println("called")
|
|
||||||
return uuid.MustParse("ef585ba0-0ac6-48f8-8ebb-2b6cd97d6a21"), nil
|
return uuid.MustParse("ef585ba0-0ac6-48f8-8ebb-2b6cd97d6a21"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,6 +235,7 @@ func setupDatabase(database library.Database) error {
|
||||||
var (
|
var (
|
||||||
Information *library.ServiceInitializationInformation
|
Information *library.ServiceInitializationInformation
|
||||||
Database library.Database
|
Database library.Database
|
||||||
|
IMAPServer *Server
|
||||||
SMTPDatabaseBackend = smtp.DatabaseBackend{
|
SMTPDatabaseBackend = smtp.DatabaseBackend{
|
||||||
CheckUser: func(address *smtp.Address) (bool, error) {
|
CheckUser: func(address *smtp.Address) (bool, error) {
|
||||||
var prefix, suffix string
|
var prefix, suffix string
|
||||||
|
@ -259,29 +258,34 @@ var (
|
||||||
return errors.New("503 5.5.1 User not found")
|
return errors.New("503 5.5.1 User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
session := IMAPServer.newSession()
|
||||||
sub: uuid.Must(uuid.FromBytes(idRaw)),
|
closeSession := func() {
|
||||||
|
err = session.Close()
|
||||||
|
if err != nil {
|
||||||
|
log("Failed to close session: "+err.Error(), 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
err = session.loginRaw(uuid.MustParse(string(idRaw)))
|
||||||
mailbox, err := user.mailbox("INBOX")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
go closeSession()
|
||||||
return errors.New("503 5.5.1 User not found")
|
return errors.New("503 5.5.1 User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
view := mailbox.NewView()
|
_, err = session.Select("INBOX", nil)
|
||||||
|
|
||||||
_, err = view.appendBuffer(bytes.NewReader(mail.Data), int64(len(mail.Data)), &imap.AppendOptions{
|
|
||||||
Time: time.Now(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = view.Close()
|
go closeSession()
|
||||||
if err != nil {
|
|
||||||
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
|
||||||
}
|
|
||||||
return errors.New("421 4.7.0 Error storing message")
|
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
|
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])
|
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)
|
sub, err := Authenticate(token, OAuthRegistration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("421 4.7.0 Invalid credentials")
|
return nil, errors.New("421 4.7.0 Invalid credentials")
|
||||||
|
|
486
message.go
486
message.go
|
@ -6,8 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-imap/v2"
|
"github.com/emersion/go-imap/v2"
|
||||||
|
@ -17,52 +17,103 @@ import (
|
||||||
"github.com/emersion/go-message/textproto"
|
"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 {
|
type message struct {
|
||||||
// immutable
|
// immutable
|
||||||
uid imap.UID
|
uid imap.UID
|
||||||
buf io.ReadCloser
|
underlyingBuf io.ReadSeekCloser
|
||||||
len int64
|
bufMutex sync.Mutex
|
||||||
t time.Time
|
len int64
|
||||||
id uuid.UUID
|
t time.Time
|
||||||
lines int64
|
id uuid.UUID
|
||||||
|
|
||||||
// mutable, protected by Mailbox.mutex
|
// mutable, protected by Mailbox.mutex
|
||||||
flags map[imap.Flag]struct{}
|
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 {
|
func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error {
|
||||||
w.WriteUID(msg.uid)
|
w.WriteUID(msg.uid)
|
||||||
|
|
||||||
if options.Flags {
|
if options.Flags {
|
||||||
fmt.Println("FLAGS: ", msg.flagList())
|
|
||||||
w.WriteFlags(msg.flagList())
|
w.WriteFlags(msg.flagList())
|
||||||
}
|
}
|
||||||
if options.InternalDate {
|
if options.InternalDate {
|
||||||
w.WriteInternalDate(msg.t)
|
w.WriteInternalDate(msg.t)
|
||||||
}
|
}
|
||||||
if options.RFC822Size {
|
if options.RFC822Size {
|
||||||
fmt.Println("RFC822SIZE: ", msg.len)
|
|
||||||
w.WriteRFC822Size(msg.len)
|
w.WriteRFC822Size(msg.len)
|
||||||
}
|
}
|
||||||
if options.Envelope {
|
if options.Envelope {
|
||||||
w.WriteEnvelope(msg.envelope())
|
w.WriteEnvelope(msg.envelope())
|
||||||
}
|
}
|
||||||
bs := options.BodyStructure
|
if options.BodyStructure != nil {
|
||||||
if bs != nil {
|
buf, err := msg.buf()
|
||||||
structure, err := msg.bodyStructure(bs.Extended)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println("BODYSTRUCTURE ERROR: ", err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
println("BODYSTRUCTURE: ", structure.Disposition().Value)
|
w.WriteBodyStructure(imapserver.ExtractBodyStructure(buf))
|
||||||
w.WriteBodyStructure(structure)
|
buf.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bs := range options.BodySection {
|
for _, bs := range options.BodySection {
|
||||||
buf := msg.bodySection(bs)
|
msgBuf, err := msg.buf()
|
||||||
wc := w.WriteBodySection(bs, msg.len)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf := imapserver.ExtractBodySection(msgBuf, bs)
|
||||||
|
wc := w.WriteBodySection(bs, int64(len(buf)))
|
||||||
_, writeErr := wc.Write(buf)
|
_, writeErr := wc.Write(buf)
|
||||||
closeErr := wc.Close()
|
closeErr := wc.Close()
|
||||||
|
msgBuf.Close()
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
return writeErr
|
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()
|
return w.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *message) envelope() *imap.Envelope {
|
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)
|
header, err := textproto.ReadHeader(br)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return getEnvelope(header)
|
buf.Close()
|
||||||
}
|
return imapserver.ExtractEnvelope(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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *message) flagList() []imap.Flag {
|
func (msg *message) flagList() []imap.Flag {
|
||||||
|
@ -251,101 +193,114 @@ func (msg *message) store(store *imap.StoreFlags) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *message) reader() *gomessage.Entity {
|
func (msg *message) reader() (*gomessage.Entity, error) {
|
||||||
r, _ := gomessage.Read(msg.buf)
|
buf, err := msg.buf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r, _ := gomessage.Read(buf)
|
||||||
|
buf.Close()
|
||||||
if r == nil {
|
if r == nil {
|
||||||
r, _ = gomessage.New(gomessage.Header{}, bytes.NewReader(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 {
|
for _, seqSet := range criteria.SeqNum {
|
||||||
if seqNum == 0 || !seqSet.Contains(seqNum) {
|
if seqNum == 0 || !seqSet.Contains(seqNum) {
|
||||||
println("DOES NOT CONTAIN 1")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, uidSet := range criteria.UID {
|
for _, uidSet := range criteria.UID {
|
||||||
if !uidSet.Contains(msg.uid) {
|
if !uidSet.Contains(msg.uid) {
|
||||||
println("DOES NOT CONTAIN 2")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !matchDate(msg.t, criteria.Since, criteria.Before) {
|
if !matchDate(msg.t, criteria.Since, criteria.Before) {
|
||||||
println("DOES NOT CONTAIN 3")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, flag := range criteria.Flag {
|
for _, flag := range criteria.Flag {
|
||||||
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
|
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
|
||||||
println("DOES NOT CONTAIN 4")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, flag := range criteria.NotFlag {
|
for _, flag := range criteria.NotFlag {
|
||||||
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
||||||
println("DOES NOT CONTAIN 5")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if criteria.Larger != 0 && msg.len <= criteria.Larger {
|
if criteria.Larger != 0 && msg.len <= criteria.Larger {
|
||||||
println("DOES NOT CONTAIN 6")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
if criteria.Smaller != 0 && msg.len >= criteria.Smaller {
|
if criteria.Smaller != 0 && msg.len >= criteria.Smaller {
|
||||||
println("DOES NOT CONTAIN 7")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
for _, fieldCriteria := range criteria.Header {
|
||||||
if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) {
|
if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) {
|
||||||
println("DOES NOT CONTAIN 8")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() {
|
if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() {
|
||||||
t, err := header.Date()
|
t, err := header.Date()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println("DOES NOT CONTAIN 9")
|
return false, err
|
||||||
return false
|
|
||||||
} else if !matchDate(t, criteria.SentSince, criteria.SentBefore) {
|
} else if !matchDate(t, criteria.SentSince, criteria.SentBefore) {
|
||||||
println("DOES NOT CONTAIN 10")
|
return false, nil
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, text := range criteria.Text {
|
for _, text := range criteria.Text {
|
||||||
if !matchEntity(msg.reader(), text, true) {
|
msgReader, err := msg.reader()
|
||||||
println("DOES NOT CONTAIN 11")
|
if err != nil {
|
||||||
return false
|
return false, err
|
||||||
|
}
|
||||||
|
if !matchEntity(msgReader, text, true) {
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, body := range criteria.Body {
|
for _, body := range criteria.Body {
|
||||||
if !matchEntity(msg.reader(), body, false) {
|
msgReader, err := msg.reader()
|
||||||
println("DOES NOT CONTAIN 12")
|
if err != nil {
|
||||||
return false
|
return false, err
|
||||||
|
}
|
||||||
|
if !matchEntity(msgReader, body, false) {
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, not := range criteria.Not {
|
for _, not := range criteria.Not {
|
||||||
if msg.search(seqNum, ¬) {
|
search, err := msg.search(seqNum, ¬)
|
||||||
println("DOES NOT CONTAIN 13")
|
if err != nil {
|
||||||
return false
|
return false, err
|
||||||
|
}
|
||||||
|
if search {
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, or := range criteria.Or {
|
for _, or := range criteria.Or {
|
||||||
if !msg.search(seqNum, &or[0]) && !msg.search(seqNum, &or[1]) {
|
search, err := msg.search(seqNum, &or[0])
|
||||||
println("DOES NOT CONTAIN 14")
|
if err != nil {
|
||||||
return false
|
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 {
|
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 {
|
func canonicalFlag(flag imap.Flag) imap.Flag {
|
||||||
return imap.Flag(strings.ToLower(string(flag)))
|
return imap.Flag(strings.ToLower(string(flag)))
|
||||||
}
|
}
|
||||||
|
|
160
session.go
160
session.go
|
@ -3,12 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"github.com/emersion/go-imap/v2"
|
"github.com/emersion/go-imap/v2"
|
||||||
"github.com/emersion/go-imap/v2/imapserver"
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
"io"
|
"github.com/google/uuid"
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
user = User
|
|
||||||
mailbox = MailboxView
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserSession represents a session tied to a specific user.
|
// UserSession represents a session tied to a specific user.
|
||||||
|
@ -16,76 +11,116 @@ type (
|
||||||
// UserSession implements imapserver.Session. Typically, a UserSession pointer
|
// UserSession implements imapserver.Session. Typically, a UserSession pointer
|
||||||
// is embedded into a larger struct which overrides Login.
|
// is embedded into a larger struct which overrides Login.
|
||||||
type UserSession struct {
|
type UserSession struct {
|
||||||
*user // immutable
|
// immutable
|
||||||
*mailbox // may be nil
|
*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.
|
type EntryPoint struct {
|
||||||
func NewUserSession(user *User) *UserSession {
|
*UserSession
|
||||||
return &UserSession{user: user}
|
*Server
|
||||||
}
|
}
|
||||||
|
|
||||||
type ByteLiteral struct {
|
func (sess *EntryPoint) Login(_, token string) (err error) {
|
||||||
io.Reader
|
sess.UserSession = &UserSession{
|
||||||
size int64
|
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 {
|
func (sess *EntryPoint) loginRaw(sub uuid.UUID) (err error) {
|
||||||
return l.size
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
view := mbox.NewView()
|
view := mbox.NewView()
|
||||||
defer func() {
|
defer func() {
|
||||||
err := view.Close()
|
err := view.close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
println("INITIAL APPEND ENTRYPOINT SUCCESS")
|
return view.appendBuffer(r, r.Size(), options)
|
||||||
return view.appendLiteral(&ByteLiteral{Reader: r, size: r.Size()}, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *UserSession) Close() error {
|
func (sess *UserSession) Close() error {
|
||||||
if sess != nil && sess.mailbox != nil {
|
if sess == nil || sess.MailboxView == nil {
|
||||||
return sess.mailbox.Close()
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *UserSession) Select(name string, _ *imap.SelectOptions) (*imap.SelectData, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sess.mailbox = mbox.NewView()
|
mbox.sessionMutex.Lock()
|
||||||
|
mbox.sessions[sess] = struct{}{}
|
||||||
|
mbox.sessionMutex.Unlock()
|
||||||
|
sess.MailboxView = mbox.NewView()
|
||||||
return mbox.selectData()
|
return mbox.selectData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *UserSession) Unselect() error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sess.mailbox = nil
|
sess.MailboxView = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (data *imap.CopyData, fErr error) {
|
func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (*imap.CopyData, error) {
|
||||||
dest, err := sess.user.mailbox(destName)
|
dest, err := sess.mailbox(destName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &imap.Error{
|
return nil, &imap.Error{
|
||||||
Type: imap.StatusResponseTypeNo,
|
Type: imap.StatusResponseTypeNo,
|
||||||
Code: imap.ResponseCodeTryCreate,
|
Code: imap.ResponseCodeTryCreate,
|
||||||
Text: "No such mailbox",
|
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{
|
return nil, &imap.Error{
|
||||||
Type: imap.StatusResponseTypeNo,
|
Type: imap.StatusResponseTypeNo,
|
||||||
Text: "Source and destination mailboxes are identical",
|
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()
|
destView := dest.NewView()
|
||||||
defer func() {
|
defer func() {
|
||||||
err := destView.Close()
|
err := destView.close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var sourceUIDs, destUIDs imap.UIDSet
|
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 {
|
||||||
appendData, err := destView.copyMsg(msg)
|
var appendData *imap.AppendData
|
||||||
|
appendData, err = destView.copyMsg(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fErr = err
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
sourceUIDs.AddNum(msg.uid)
|
sourceUIDs.AddNum(msg.uid)
|
||||||
destUIDs.AddNum(appendData.UID)
|
destUIDs.AddNum(appendData.UID)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if fErr != nil {
|
|
||||||
return nil, fErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return &imap.CopyData{
|
return &imap.CopyData{
|
||||||
UIDValidity: dest.UIDValidity,
|
UIDValidity: dest.UIDValidity,
|
||||||
|
@ -124,15 +157,15 @@ func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (data *imap.C
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) (fErr error) {
|
func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) error {
|
||||||
dest, err := sess.user.mailbox(destName)
|
dest, err := sess.mailbox(destName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &imap.Error{
|
return &imap.Error{
|
||||||
Type: imap.StatusResponseTypeNo,
|
Type: imap.StatusResponseTypeNo,
|
||||||
Code: imap.ResponseCodeTryCreate,
|
Code: imap.ResponseCodeTryCreate,
|
||||||
Text: "No such mailbox",
|
Text: "No such mailbox",
|
||||||
}
|
}
|
||||||
} else if sess.mailbox != nil && dest == sess.mailbox.Mailbox {
|
} else if sess.MailboxView != nil && dest == sess.Mailbox {
|
||||||
return &imap.Error{
|
return &imap.Error{
|
||||||
Type: imap.StatusResponseTypeNo,
|
Type: imap.StatusResponseTypeNo,
|
||||||
Text: "Source and destination mailboxes are identical",
|
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()
|
destView := dest.NewView()
|
||||||
defer func() {
|
defer func() {
|
||||||
err := destView.Close()
|
err := destView.close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
log("Failed to close view: "+err.Error()+", resource leaks may occur", 1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var sourceUIDs, destUIDs imap.UIDSet
|
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
|
var appendData *imap.AppendData
|
||||||
appendData, fErr = destView.copyMsg(msg)
|
appendData, err = destView.copyMsg(msg)
|
||||||
if fErr != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
sourceUIDs.AddNum(msg.uid)
|
sourceUIDs.AddNum(msg.uid)
|
||||||
destUIDs.AddNum(appendData.UID)
|
destUIDs.AddNum(appendData.UID)
|
||||||
})
|
return nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if fErr != nil {
|
|
||||||
return fErr
|
|
||||||
}
|
|
||||||
|
|
||||||
err = w.WriteCopyData(&imap.CopyData{
|
|
||||||
UIDValidity: dest.UIDValidity,
|
|
||||||
SourceUIDs: sourceUIDs,
|
|
||||||
DestUIDs: destUIDs,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
println(sourceUIDs.String())
|
if len(sourceUIDs) != 0 && len(destUIDs) != 0 {
|
||||||
err = sess.mailbox.Expunge(nil, &sourceUIDs)
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
func (sess *UserSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
|
||||||
if sess.mailbox == nil {
|
if sess.MailboxView == nil {
|
||||||
return 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 {
|
func (sess *UserSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
|
||||||
if sess.mailbox == nil {
|
if sess.MailboxView == nil {
|
||||||
return nil // TODO
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/emersion/go-imap/v2"
|
"github.com/emersion/go-imap/v2"
|
||||||
"github.com/emersion/go-imap/v2/imapserver"
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const mailboxDelim rune = '/'
|
const mailboxDelim rune = '/'
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
server *Server // may be nil if sub is not set
|
server *Server // may be nil if sub is not set
|
||||||
sub uuid.UUID
|
mailboxMutex sync.Mutex
|
||||||
}
|
mailboxes map[string]*Mailbox
|
||||||
|
sessionMutex sync.Mutex
|
||||||
func (u *User) Login(_, token string) error {
|
sessions map[*UserSession]struct{}
|
||||||
sub, err := Authenticate(token, u.server.config)
|
sub uuid.UUID
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) mailbox(name string) (*Mailbox, error) {
|
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) {
|
func (u *User) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) {
|
||||||
|
|
Loading…
Reference in New Issue