A webmail client. Forked from https://git.sr.ht/~migadu/alps
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

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