From a393429f01e63aa37f58f8cbe4a810e59852fa61 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Thu, 29 Oct 2020 15:18:36 -0400 Subject: Implement JavaScript UI for attachments This one is a bit of a doozy. A summary of the changes: - Session has grown storage for attachments which have been uploaded but not yet sent. - The list of attachments on a message is refcounted so that we can clean up the temporary files only after it's done with - i.e. after copying to Sent and after all of the SMTP attempts are done. - Abandoned attachments are cleared out on process shutdown. Future work: - Add a limit to the maximum number of pending attachments the user can have in the session. - Periodically clean out abandoned attachments? --- session.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 7 deletions(-) (limited to 'session.go') diff --git a/session.go b/session.go index 9428cd9..0aca9ed 100644 --- a/session.go +++ b/session.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "fmt" + "mime/multipart" "net/http" "os" "sync" @@ -13,6 +14,7 @@ import ( imapclient "github.com/emersion/go-imap/client" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" + "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -54,6 +56,14 @@ type Session struct { imapLocker sync.Mutex imapConn *imapclient.Client // protected by locker, can be nil + + attachmentsLocker sync.Mutex + attachments map[string]*Attachment // protected by attachmentsLocker +} + +type Attachment struct { + File *multipart.FileHeader + Form *multipart.Form } func (s *Session) ping() { @@ -117,6 +127,13 @@ func (s *Session) SetHTTPBasicAuth(req *http.Request) { // Close destroys the session. This can be used to log the user out. func (s *Session) Close() { + s.attachmentsLocker.Lock() + defer s.attachmentsLocker.Unlock() + + for _, f := range s.attachments { + f.Form.RemoveAll() + } + select { case <-s.closed: // This space is intentionally left blank @@ -125,6 +142,41 @@ func (s *Session) Close() { } } +// Puts an attachment and returns a generated UUID +func (s *Session) PutAttachment(in *multipart.FileHeader, + form *multipart.Form) (string, error) { + // TODO: Prevent users from uploading too many attachments, or too large + // + // Probably just set a cap on the maximum combined size of all files in the + // user's session + // + // TODO: Figure out what to do if the user abandons the compose window + // after adding some attachments + id := uuid.New() + s.attachmentsLocker.Lock() + s.attachments[id.String()] = &Attachment{ + File: in, + Form: form, + } + s.attachmentsLocker.Unlock() + return id.String(), nil +} + +// Removes an attachment from the session. Returns nil if there was no such +// attachment. +func (s *Session) PopAttachment(uuid string) *Attachment { + s.attachmentsLocker.Lock() + defer s.attachmentsLocker.Unlock() + + a, ok := s.attachments[uuid] + if !ok { + return nil + } + delete(s.attachments, uuid) + + return a +} + // Store returns a store suitable for storing persistent user data. func (s *Session) Store() Store { return s.store @@ -159,6 +211,12 @@ func newSessionManager(dialIMAP DialIMAPFunc, dialSMTP DialSMTPFunc, logger echo } } +func (sm *SessionManager) Close() { + for _, s := range sm.sessions { + s.Close() + } +} + func (sm *SessionManager) connectIMAP(username, password string) (*imapclient.Client, error) { c, err := sm.dialIMAP() if err != nil { @@ -213,13 +271,14 @@ func (sm *SessionManager) Put(username, password string) (*Session, error) { } s := &Session{ - manager: sm, - closed: make(chan struct{}), - pings: make(chan struct{}, 5), - imapConn: c, - username: username, - password: password, - token: token, + manager: sm, + closed: make(chan struct{}), + pings: make(chan struct{}, 5), + imapConn: c, + username: username, + password: password, + token: token, + attachments: make(map[string]*Attachment), } s.store, err = newStore(s, sm.logger) -- cgit v1.2.3-59-g8ed1b