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.
 
 
 
 

257 lines
5.4 KiB

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