Testing time

This commit is contained in:
Tracker-Friendly 2025-03-20 18:37:09 +00:00
parent b80c3c2fa3
commit 6def9f892b
8 changed files with 535 additions and 680 deletions

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
View File

@ -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")

View File

@ -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, &not) {
println("DOES NOT CONTAIN 13")
return false
search, err := msg.search(seqNum, &not)
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)))
}

View File

@ -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
View File

@ -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) {