A webmail client. Forked from https://git.sr.ht/~migadu/alps
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

214 linhas
4.4 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. // Server holds all the koushin server state.
  12. type Server struct {
  13. Sessions *SessionManager
  14. Plugins []Plugin
  15. imap struct {
  16. host string
  17. tls bool
  18. insecure bool
  19. }
  20. smtp struct {
  21. host string
  22. tls bool
  23. insecure bool
  24. }
  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. if smtpURL != "" {
  68. if err := s.parseSMTPURL(smtpURL); err != nil {
  69. return nil, err
  70. }
  71. }
  72. s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP)
  73. return s, nil
  74. }
  75. // Context is the context used by HTTP handlers.
  76. //
  77. // Use a type assertion to get it from a echo.Context:
  78. //
  79. // ctx := ectx.(*koushin.Context)
  80. type Context struct {
  81. echo.Context
  82. Server *Server
  83. Session *Session // nil if user isn't logged in
  84. }
  85. var aLongTimeAgo = time.Unix(233431200, 0)
  86. // SetSession sets a cookie for the provided session. Passing a nil session
  87. // unsets the cookie.
  88. func (ctx *Context) SetSession(s *Session) {
  89. cookie := http.Cookie{
  90. Name: cookieName,
  91. HttpOnly: true,
  92. // TODO: domain, secure
  93. }
  94. if s != nil {
  95. cookie.Value = s.token
  96. } else {
  97. cookie.Expires = aLongTimeAgo // unset the cookie
  98. }
  99. ctx.SetCookie(&cookie)
  100. }
  101. func isPublic(path string) bool {
  102. if strings.HasPrefix(path, "/plugins/") {
  103. parts := strings.Split(path, "/")
  104. return len(parts) >= 4 && parts[3] == "assets"
  105. }
  106. return path == "/login" || strings.HasPrefix(path, "/themes/")
  107. }
  108. type Options struct {
  109. IMAPURL, SMTPURL string
  110. Theme string
  111. }
  112. // New creates a new server.
  113. func New(e *echo.Echo, options *Options) (*Server, error) {
  114. s, err := newServer(options.IMAPURL, options.SMTPURL)
  115. if err != nil {
  116. return nil, err
  117. }
  118. s.Plugins = append([]Plugin(nil), plugins...)
  119. for _, p := range s.Plugins {
  120. e.Logger.Printf("Registered plugin '%v'", p.Name())
  121. }
  122. luaPlugins, err := loadAllLuaPlugins(e.Logger)
  123. if err != nil {
  124. return nil, fmt.Errorf("failed to load plugins: %v", err)
  125. }
  126. s.Plugins = append(s.Plugins, luaPlugins...)
  127. e.Renderer, err = loadTemplates(e.Logger, options.Theme, s.Plugins)
  128. if err != nil {
  129. return nil, fmt.Errorf("failed to load templates: %v", err)
  130. }
  131. e.HTTPErrorHandler = func(err error, c echo.Context) {
  132. code := http.StatusInternalServerError
  133. if he, ok := err.(*echo.HTTPError); ok {
  134. code = he.Code
  135. } else {
  136. c.Logger().Error(err)
  137. }
  138. // TODO: hide internal errors
  139. c.String(code, err.Error())
  140. }
  141. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  142. return func(ectx echo.Context) error {
  143. ectx.Response().Header().Set("Content-Security-Policy", "default-src 'self'")
  144. return next(ectx)
  145. }
  146. })
  147. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  148. return func(ectx echo.Context) error {
  149. ctx := &Context{Context: ectx, Server: s}
  150. ctx.Set("context", ctx)
  151. cookie, err := ctx.Cookie(cookieName)
  152. if err == http.ErrNoCookie {
  153. // Require auth for all pages except /login
  154. if isPublic(ctx.Path()) {
  155. return next(ctx)
  156. } else {
  157. return ctx.Redirect(http.StatusFound, "/login")
  158. }
  159. } else if err != nil {
  160. return err
  161. }
  162. ctx.Session, err = ctx.Server.Sessions.get(cookie.Value)
  163. if err == errSessionExpired {
  164. ctx.SetSession(nil)
  165. return ctx.Redirect(http.StatusFound, "/login")
  166. } else if err != nil {
  167. return err
  168. }
  169. ctx.Session.ping()
  170. return next(ctx)
  171. }
  172. })
  173. e.Static("/themes", "themes")
  174. for _, p := range s.Plugins {
  175. p.SetRoutes(e.Group(""))
  176. }
  177. return s, nil
  178. }