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.
 
 
 
 

352 lines
7.4 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. notice string
  53. imapLocker sync.Mutex
  54. imapConn *imapclient.Client // protected by locker, can be nil
  55. attachmentsLocker sync.Mutex
  56. attachments map[string]*Attachment // protected by attachmentsLocker
  57. }
  58. type Attachment struct {
  59. File *multipart.FileHeader
  60. Form *multipart.Form
  61. }
  62. func (s *Session) ping() {
  63. s.pings <- struct{}{}
  64. }
  65. // Username returns the session's username.
  66. func (s *Session) Username() string {
  67. return s.username
  68. }
  69. // DoIMAP executes an IMAP operation on this session. The IMAP client can only
  70. // be used from inside f.
  71. func (s *Session) DoIMAP(f func(*imapclient.Client) error) error {
  72. s.imapLocker.Lock()
  73. defer s.imapLocker.Unlock()
  74. if s.imapConn == nil {
  75. var err error
  76. s.imapConn, err = s.manager.connectIMAP(s.username, s.password)
  77. if err != nil {
  78. s.Close()
  79. return fmt.Errorf("failed to re-connect to IMAP server: %v", err)
  80. }
  81. }
  82. return f(s.imapConn)
  83. }
  84. // DoSMTP executes an SMTP operation on this session. The SMTP client can only
  85. // be used from inside f.
  86. func (s *Session) DoSMTP(f func(*smtp.Client) error) error {
  87. c, err := s.manager.dialSMTP()
  88. if err != nil {
  89. return err
  90. }
  91. defer c.Close()
  92. auth := sasl.NewPlainClient("", s.username, s.password)
  93. if err := c.Auth(auth); err != nil {
  94. return AuthError{err}
  95. }
  96. if err := f(c); err != nil {
  97. return err
  98. }
  99. if err := c.Quit(); err != nil {
  100. return fmt.Errorf("QUIT failed: %v", err)
  101. }
  102. return nil
  103. }
  104. // SetHTTPBasicAuth adds an Authorization header field to the request with
  105. // this session's credentials.
  106. func (s *Session) SetHTTPBasicAuth(req *http.Request) {
  107. // TODO: find a way to make it harder for plugins to steal credentials
  108. req.SetBasicAuth(s.username, s.password)
  109. }
  110. // Close destroys the session. This can be used to log the user out.
  111. func (s *Session) Close() {
  112. s.attachmentsLocker.Lock()
  113. defer s.attachmentsLocker.Unlock()
  114. for _, f := range s.attachments {
  115. f.Form.RemoveAll()
  116. }
  117. select {
  118. case <-s.closed:
  119. // This space is intentionally left blank
  120. default:
  121. close(s.closed)
  122. }
  123. }
  124. // Puts an attachment and returns a generated UUID
  125. func (s *Session) PutAttachment(in *multipart.FileHeader,
  126. form *multipart.Form) (string, error) {
  127. id := uuid.New()
  128. s.attachmentsLocker.Lock()
  129. var size int64
  130. for _, a := range s.attachments {
  131. size += a.File.Size
  132. }
  133. if size + in.Size > maxAttachmentSize {
  134. return "", ErrAttachmentCacheSize
  135. }
  136. s.attachments[id.String()] = &Attachment{
  137. File: in,
  138. Form: form,
  139. }
  140. s.attachmentsLocker.Unlock()
  141. return id.String(), nil
  142. }
  143. // Removes an attachment from the session. Returns nil if there was no such
  144. // attachment.
  145. func (s *Session) PopAttachment(uuid string) *Attachment {
  146. s.attachmentsLocker.Lock()
  147. defer s.attachmentsLocker.Unlock()
  148. a, ok := s.attachments[uuid]
  149. if !ok {
  150. return nil
  151. }
  152. delete(s.attachments, uuid)
  153. return a
  154. }
  155. func (s *Session) PutNotice(n string) {
  156. s.notice = n
  157. }
  158. func (s *Session) PopNotice() string {
  159. n := s.notice
  160. s.notice = ""
  161. return n
  162. }
  163. // Store returns a store suitable for storing persistent user data.
  164. func (s *Session) Store() Store {
  165. return s.store
  166. }
  167. type (
  168. // DialIMAPFunc connects to the upstream IMAP server.
  169. DialIMAPFunc func() (*imapclient.Client, error)
  170. // DialSMTPFunc connects to the upstream SMTP server.
  171. DialSMTPFunc func() (*smtp.Client, error)
  172. )
  173. // SessionManager keeps track of active sessions. It connects and re-connects
  174. // to the upstream IMAP server as necessary. It prunes expired sessions.
  175. type SessionManager struct {
  176. dialIMAP DialIMAPFunc
  177. dialSMTP DialSMTPFunc
  178. logger echo.Logger
  179. debug bool
  180. locker sync.Mutex
  181. sessions map[string]*Session // protected by locker
  182. }
  183. func newSessionManager(dialIMAP DialIMAPFunc, dialSMTP DialSMTPFunc, logger echo.Logger, debug bool) *SessionManager {
  184. return &SessionManager{
  185. sessions: make(map[string]*Session),
  186. dialIMAP: dialIMAP,
  187. dialSMTP: dialSMTP,
  188. logger: logger,
  189. debug: debug,
  190. }
  191. }
  192. func (sm *SessionManager) Close() {
  193. for _, s := range sm.sessions {
  194. s.Close()
  195. }
  196. }
  197. func (sm *SessionManager) connectIMAP(username, password string) (*imapclient.Client, error) {
  198. c, err := sm.dialIMAP()
  199. if err != nil {
  200. return nil, err
  201. }
  202. if err := c.Login(username, password); err != nil {
  203. c.Logout()
  204. return nil, AuthError{err}
  205. }
  206. if sm.debug {
  207. c.SetDebug(os.Stderr)
  208. }
  209. return c, nil
  210. }
  211. func (sm *SessionManager) get(token string) (*Session, error) {
  212. sm.locker.Lock()
  213. defer sm.locker.Unlock()
  214. session, ok := sm.sessions[token]
  215. if !ok {
  216. return nil, ErrSessionExpired
  217. }
  218. return session, nil
  219. }
  220. // Put connects to the IMAP server and creates a new session. If authentication
  221. // fails, the error will be of type AuthError.
  222. func (sm *SessionManager) Put(username, password string) (*Session, error) {
  223. c, err := sm.connectIMAP(username, password)
  224. if err != nil {
  225. return nil, err
  226. }
  227. sm.locker.Lock()
  228. defer sm.locker.Unlock()
  229. var token string
  230. for {
  231. token, err = generateToken()
  232. if err != nil {
  233. c.Logout()
  234. return nil, err
  235. }
  236. if _, ok := sm.sessions[token]; !ok {
  237. break
  238. }
  239. }
  240. s := &Session{
  241. manager: sm,
  242. closed: make(chan struct{}),
  243. pings: make(chan struct{}, 5),
  244. imapConn: c,
  245. username: username,
  246. password: password,
  247. token: token,
  248. attachments: make(map[string]*Attachment),
  249. }
  250. s.store, err = newStore(s, sm.logger)
  251. if err != nil {
  252. return nil, err
  253. }
  254. sm.sessions[token] = s
  255. go func() {
  256. timer := time.NewTimer(sessionDuration)
  257. alive := true
  258. for alive {
  259. var loggedOut <-chan struct{}
  260. s.imapLocker.Lock()
  261. if s.imapConn != nil {
  262. loggedOut = s.imapConn.LoggedOut()
  263. }
  264. s.imapLocker.Unlock()
  265. select {
  266. case <-loggedOut:
  267. s.imapLocker.Lock()
  268. s.imapConn = nil
  269. s.imapLocker.Unlock()
  270. case <-s.pings:
  271. if !timer.Stop() {
  272. <-timer.C
  273. }
  274. timer.Reset(sessionDuration)
  275. case <-timer.C:
  276. alive = false
  277. case <-s.closed:
  278. alive = false
  279. }
  280. }
  281. timer.Stop()
  282. s.imapLocker.Lock()
  283. if s.imapConn != nil {
  284. s.imapConn.Logout()
  285. }
  286. s.imapLocker.Unlock()
  287. sm.locker.Lock()
  288. delete(sm.sessions, token)
  289. sm.locker.Unlock()
  290. }()
  291. return s, nil
  292. }