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.
 
 
 
 

208 lines
4.1 KiB

  1. package koushin
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "strings"
  7. "time"
  8. "github.com/labstack/echo/v4"
  9. )
  10. const cookieName = "koushin_session"
  11. const messagesPerPage = 50
  12. type Server struct {
  13. imap struct {
  14. host string
  15. tls bool
  16. insecure bool
  17. pool *ConnPool
  18. }
  19. smtp struct {
  20. host string
  21. tls bool
  22. insecure bool
  23. }
  24. plugins []Plugin
  25. }
  26. func (s *Server) parseIMAPURL(imapURL string) error {
  27. u, err := url.Parse(imapURL)
  28. if err != nil {
  29. return fmt.Errorf("failed to parse IMAP server URL: %v", err)
  30. }
  31. s.imap.host = u.Host
  32. switch u.Scheme {
  33. case "imap":
  34. // This space is intentionally left blank
  35. case "imaps":
  36. s.imap.tls = true
  37. case "imap+insecure":
  38. s.imap.insecure = true
  39. default:
  40. return fmt.Errorf("unrecognized IMAP URL scheme: %s", u.Scheme)
  41. }
  42. return nil
  43. }
  44. func (s *Server) parseSMTPURL(smtpURL string) error {
  45. u, err := url.Parse(smtpURL)
  46. if err != nil {
  47. return fmt.Errorf("failed to parse SMTP server URL: %v", err)
  48. }
  49. s.smtp.host = u.Host
  50. switch u.Scheme {
  51. case "smtp":
  52. // This space is intentionally left blank
  53. case "smtps":
  54. s.smtp.tls = true
  55. case "smtp+insecure":
  56. s.smtp.insecure = true
  57. default:
  58. return fmt.Errorf("unrecognized SMTP URL scheme: %s", u.Scheme)
  59. }
  60. return nil
  61. }
  62. func newServer(imapURL, smtpURL string) (*Server, error) {
  63. s := &Server{}
  64. if err := s.parseIMAPURL(imapURL); err != nil {
  65. return nil, err
  66. }
  67. s.imap.pool = NewConnPool()
  68. if smtpURL != "" {
  69. if err := s.parseSMTPURL(smtpURL); err != nil {
  70. return nil, err
  71. }
  72. }
  73. return s, nil
  74. }
  75. type context struct {
  76. echo.Context
  77. server *Server
  78. session *Session
  79. }
  80. var aLongTimeAgo = time.Unix(233431200, 0)
  81. func (c *context) setToken(token string) {
  82. cookie := http.Cookie{
  83. Name: cookieName,
  84. Value: token,
  85. HttpOnly: true,
  86. // TODO: domain, secure
  87. }
  88. if token == "" {
  89. cookie.Expires = aLongTimeAgo // unset the cookie
  90. }
  91. c.SetCookie(&cookie)
  92. }
  93. func isPublic(path string) bool {
  94. return path == "/login" || strings.HasPrefix(path, "/assets/") ||
  95. strings.HasPrefix(path, "/themes/")
  96. }
  97. type Options struct {
  98. IMAPURL, SMTPURL string
  99. Theme string
  100. }
  101. func New(e *echo.Echo, options *Options) error {
  102. s, err := newServer(options.IMAPURL, options.SMTPURL)
  103. if err != nil {
  104. return err
  105. }
  106. e.Renderer, err = loadTemplates(e.Logger, options.Theme)
  107. if err != nil {
  108. return fmt.Errorf("failed to load templates: %v", err)
  109. }
  110. s.plugins, err = loadAllLuaPlugins(e.Logger)
  111. if err != nil {
  112. return fmt.Errorf("failed to load plugins: %v", err)
  113. }
  114. e.HTTPErrorHandler = func(err error, c echo.Context) {
  115. code := http.StatusInternalServerError
  116. if he, ok := err.(*echo.HTTPError); ok {
  117. code = he.Code
  118. } else {
  119. c.Logger().Error(err)
  120. }
  121. // TODO: hide internal errors
  122. c.String(code, err.Error())
  123. }
  124. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  125. return func(ectx echo.Context) error {
  126. ctx := &context{Context: ectx, server: s}
  127. ctx.Set("context", ctx)
  128. cookie, err := ctx.Cookie(cookieName)
  129. if err == http.ErrNoCookie {
  130. // Require auth for all pages except /login
  131. if isPublic(ctx.Path()) {
  132. return next(ctx)
  133. } else {
  134. return ctx.Redirect(http.StatusFound, "/login")
  135. }
  136. } else if err != nil {
  137. return err
  138. }
  139. ctx.session, err = ctx.server.imap.pool.Get(cookie.Value)
  140. if err == ErrSessionExpired {
  141. ctx.setToken("")
  142. return ctx.Redirect(http.StatusFound, "/login")
  143. } else if err != nil {
  144. return err
  145. }
  146. return next(ctx)
  147. }
  148. })
  149. e.GET("/mailbox/:mbox", handleGetMailbox)
  150. e.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
  151. ctx := ectx.(*context)
  152. return handleGetPart(ctx, false)
  153. })
  154. e.GET("/message/:mbox/:uid/raw", func(ectx echo.Context) error {
  155. ctx := ectx.(*context)
  156. return handleGetPart(ctx, true)
  157. })
  158. e.GET("/login", handleLogin)
  159. e.POST("/login", handleLogin)
  160. e.GET("/logout", handleLogout)
  161. e.GET("/compose", handleCompose)
  162. e.POST("/compose", handleCompose)
  163. e.GET("/message/:mbox/:uid/reply", handleCompose)
  164. e.POST("/message/:mbox/:uid/reply", handleCompose)
  165. e.Static("/assets", "public/assets")
  166. e.Static("/themes", "public/themes")
  167. return nil
  168. }