A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

341 lines
7.2 KiB

  1. package alps
  2. import (
  3. "crypto/rand"
  4. "encoding/base64"
  5. "errors"
  6. "fmt"
  7. "mime/multipart"
  8. "net/http"
  9. "os"
  10. "sync"
  11. "time"
  12. imapclient "github.com/emersion/go-imap/client"
  13. "github.com/emersion/go-sasl"
  14. "github.com/emersion/go-smtp"
  15. "github.com/google/uuid"
  16. "github.com/labstack/echo/v4"
  17. )
  18. // TODO: make this configurable
  19. const sessionDuration = 30 * time.Minute
  20. const maxAttachmentSize = 32 << 20 // 32 MiB
  21. func generateToken() (string, error) {
  22. b := make([]byte, 32)
  23. _, err := rand.Read(b)
  24. if err != nil {
  25. return "", err
  26. }
  27. return base64.URLEncoding.EncodeToString(b), nil
  28. }
  29. var (
  30. ErrSessionExpired = errors.New("session expired")
  31. ErrAttachmentCacheSize = errors.New("Attachments on session exceed maximum file size")
  32. )
  33. // AuthError wraps an authentication error.
  34. type AuthError struct {
  35. cause error
  36. }
  37. func (err AuthError) Error() string {
  38. return fmt.Sprintf("authentication failed: %v", err.cause)
  39. }
  40. // Session is an active user session. It may also hold an IMAP connection.
  41. //
  42. // The session's password is not available to plugins. Plugins should use the
  43. // session helpers to authenticate outgoing connections, for instance DoSMTP.
  44. type Session struct {
  45. manager *SessionManager
  46. username, password string
  47. token string
  48. closed chan struct{}
  49. pings chan struct{}
  50. timer *time.Timer
  51. store Store
  52. imapLocker sync.Mutex
  53. imapConn *imapclient.Client // protected by locker, can be nil
  54. attachmentsLocker sync.Mutex
  55. attachments map[string]*Attachment // protected by attachmentsLocker
  56. }
  57. type Attachment struct {
  58. File *multipart.FileHeader
  59. Form *multipart.Form
  60. }
  61. func (s *Session) ping() {
  62. s.pings <- struct{}{}
  63. }
  64. // Username returns the session's username.
  65. func (s *Session) Username() string {
  66. return s.username
  67. }
  68. // DoIMAP executes an IMAP operation on this session. The IMAP client can only
  69. // be used from inside f.
  70. func (s *Session) DoIMAP(f func(*imapclient.Client) error) error {
  71. s.imapLocker.Lock()
  72. defer s.imapLocker.Unlock()
  73. if s.imapConn == nil {
  74. var err error
  75. s.imapConn, err = s.manager.connectIMAP(s.username, s.password)
  76. if err != nil {
  77. s.Close()
  78. return fmt.Errorf("failed to re-connect to IMAP server: %v", err)
  79. }
  80. }
  81. return f(s.imapConn)
  82. }
  83. // DoSMTP executes an SMTP operation on this session. The SMTP client can only
  84. // be used from inside f.
  85. func (s *Session) DoSMTP(f func(*smtp.Client) error) error {
  86. c, err := s.manager.dialSMTP()
  87. if err != nil {
  88. return err
  89. }
  90. defer c.Close()
  91. auth := sasl.NewPlainClient("", s.username, s.password)
  92. if err := c.Auth(auth); err != nil {
  93. return AuthError{err}
  94. }
  95. if err := f(c); err != nil {
  96. return err
  97. }
  98. if err := c.Quit(); err != nil {
  99. return fmt.Errorf("QUIT failed: %v", err)
  100. }
  101. return nil
  102. }
  103. // SetHTTPBasicAuth adds an Authorization header field to the request with
  104. // this session's credentials.
  105. func (s *Session) SetHTTPBasicAuth(req *http.Request) {
  106. // TODO: find a way to make it harder for plugins to steal credentials
  107. req.SetBasicAuth(s.username, s.password)
  108. }
  109. // Close destroys the session. This can be used to log the user out.
  110. func (s *Session) Close() {
  111. s.attachmentsLocker.Lock()
  112. defer s.attachmentsLocker.Unlock()
  113. for _, f := range s.attachments {
  114. f.Form.RemoveAll()
  115. }
  116. select {
  117. case <-s.closed:
  118. // This space is intentionally left blank
  119. default:
  120. close(s.closed)
  121. }
  122. }
  123. // Puts an attachment and returns a generated UUID
  124. func (s *Session) PutAttachment(in *multipart.FileHeader,
  125. form *multipart.Form) (string, error) {
  126. id := uuid.New()
  127. s.attachmentsLocker.Lock()
  128. var size int64
  129. for _, a := range s.attachments {
  130. size += a.File.Size
  131. }
  132. if size + in.Size > maxAttachmentSize {
  133. return "", ErrAttachmentCacheSize
  134. }
  135. s.attachments[id.String()] = &Attachment{
  136. File: in,
  137. Form: form,
  138. }
  139. s.attachmentsLocker.Unlock()
  140. return id.String(), nil
  141. }
  142. // Removes an attachment from the session. Returns nil if there was no such
  143. // attachment.
  144. func (s *Session) PopAttachment(uuid string) *Attachment {
  145. s.attachmentsLocker.Lock()
  146. defer s.attachmentsLocker.Unlock()
  147. a, ok := s.attachments[uuid]
  148. if !ok {
  149. return nil
  150. }
  151. delete(s.attachments, uuid)
  152. return a
  153. }
  154. // Store returns a store suitable for storing persistent user data.
  155. func (s *Session) Store() Store {
  156. return s.store
  157. }
  158. type (
  159. // DialIMAPFunc connects to the upstream IMAP server.
  160. DialIMAPFunc func() (*imapclient.Client, error)
  161. // DialSMTPFunc connects to the upstream SMTP server.
  162. DialSMTPFunc func() (*smtp.Client, error)
  163. )
  164. // SessionManager keeps track of active sessions. It connects and re-connects
  165. // to the upstream IMAP server as necessary. It prunes expired sessions.
  166. type SessionManager struct {
  167. dialIMAP DialIMAPFunc
  168. dialSMTP DialSMTPFunc
  169. logger echo.Logger
  170. debug bool
  171. locker sync.Mutex
  172. sessions map[string]*Session // protected by locker
  173. }
  174. func newSessionManager(dialIMAP DialIMAPFunc, dialSMTP DialSMTPFunc, logger echo.Logger, debug bool) *SessionManager {
  175. return &SessionManager{
  176. sessions: make(map[string]*Session),
  177. dialIMAP: dialIMAP,
  178. dialSMTP: dialSMTP,
  179. logger: logger,
  180. debug: debug,
  181. }
  182. }
  183. func (sm *SessionManager) Close() {
  184. for _, s := range sm.sessions {
  185. s.Close()
  186. }
  187. }
  188. func (sm *SessionManager) connectIMAP(username, password string) (*imapclient.Client, error) {
  189. c, err := sm.dialIMAP()
  190. if err != nil {
  191. return nil, err
  192. }
  193. if err := c.Login(username, password); err != nil {
  194. c.Logout()
  195. return nil, AuthError{err}
  196. }
  197. if sm.debug {
  198. c.SetDebug(os.Stderr)
  199. }
  200. return c, nil
  201. }
  202. func (sm *SessionManager) get(token string) (*Session, error) {
  203. sm.locker.Lock()
  204. defer sm.locker.Unlock()
  205. session, ok := sm.sessions[token]
  206. if !ok {
  207. return nil, ErrSessionExpired
  208. }
  209. return session, nil
  210. }
  211. // Put connects to the IMAP server and creates a new session. If authentication
  212. // fails, the error will be of type AuthError.
  213. func (sm *SessionManager) Put(username, password string) (*Session, error) {
  214. c, err := sm.connectIMAP(username, password)
  215. if err != nil {
  216. return nil, err
  217. }
  218. sm.locker.Lock()
  219. defer sm.locker.Unlock()
  220. var token string
  221. for {
  222. token, err = generateToken()
  223. if err != nil {
  224. c.Logout()
  225. return nil, err
  226. }
  227. if _, ok := sm.sessions[token]; !ok {
  228. break
  229. }
  230. }
  231. s := &Session{
  232. manager: sm,
  233. closed: make(chan struct{}),
  234. pings: make(chan struct{}, 5),
  235. imapConn: c,
  236. username: username,
  237. password: password,
  238. token: token,
  239. attachments: make(map[string]*Attachment),
  240. }
  241. s.store, err = newStore(s, sm.logger)
  242. if err != nil {
  243. return nil, err
  244. }
  245. sm.sessions[token] = s
  246. go func() {
  247. timer := time.NewTimer(sessionDuration)
  248. alive := true
  249. for alive {
  250. var loggedOut <-chan struct{}
  251. s.imapLocker.Lock()
  252. if s.imapConn != nil {
  253. loggedOut = s.imapConn.LoggedOut()
  254. }
  255. s.imapLocker.Unlock()
  256. select {
  257. case <-loggedOut:
  258. s.imapLocker.Lock()
  259. s.imapConn = nil
  260. s.imapLocker.Unlock()
  261. case <-s.pings:
  262. if !timer.Stop() {
  263. <-timer.C
  264. }
  265. timer.Reset(sessionDuration)
  266. case <-timer.C:
  267. alive = false
  268. case <-s.closed:
  269. alive = false
  270. }
  271. }
  272. timer.Stop()
  273. s.imapLocker.Lock()
  274. if s.imapConn != nil {
  275. s.imapConn.Logout()
  276. }
  277. s.imapLocker.Unlock()
  278. sm.locker.Lock()
  279. delete(sm.sessions, token)
  280. sm.locker.Unlock()
  281. }()
  282. return s, nil
  283. }