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.
 
 
 
 

276 lines
5.8 KiB

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