A webmail client. Forked from https://git.sr.ht/~migadu/alps
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

355 行
8.1 KiB

  1. package koushin
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/labstack/echo/v4"
  10. )
  11. const cookieName = "koushin_session"
  12. // Server holds all the koushin server state.
  13. type Server struct {
  14. e *echo.Echo
  15. Sessions *SessionManager
  16. mutex sync.RWMutex // used for server reload
  17. plugins []Plugin
  18. luaPlugins []Plugin
  19. // maps protocols to URLs (protocol can be empty for auto-discovery)
  20. upstreams map[string]*url.URL
  21. imap struct {
  22. host string
  23. tls bool
  24. insecure bool
  25. }
  26. smtp struct {
  27. host string
  28. tls bool
  29. insecure bool
  30. }
  31. defaultTheme string
  32. }
  33. func newServer(e *echo.Echo, options *Options) (*Server, error) {
  34. s := &Server{e: e, defaultTheme: options.Theme}
  35. s.upstreams = make(map[string]*url.URL, len(options.Upstreams))
  36. for _, upstream := range options.Upstreams {
  37. u, err := parseUpstream(upstream)
  38. if err != nil {
  39. return nil, fmt.Errorf("failed to parse upstream %q: %v", upstream, err)
  40. }
  41. if _, ok := s.upstreams[u.Scheme]; ok {
  42. return nil, fmt.Errorf("found two upstream servers for scheme %q", u.Scheme)
  43. }
  44. s.upstreams[u.Scheme] = u
  45. }
  46. if err := s.parseIMAPUpstream(); err != nil {
  47. return nil, err
  48. }
  49. if err := s.parseSMTPUpstream(); err != nil {
  50. return nil, err
  51. }
  52. s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP)
  53. return s, nil
  54. }
  55. func parseUpstream(s string) (*url.URL, error) {
  56. if !strings.ContainsAny(s, ":/") {
  57. // This is a raw domain name, make it an URL with an empty scheme
  58. s = "//" + s
  59. }
  60. return url.Parse(s)
  61. }
  62. type NoUpstreamError struct {
  63. schemes []string
  64. }
  65. func (err *NoUpstreamError) Error() string {
  66. return fmt.Sprintf("no upstream server configured for schemes %v", err.schemes)
  67. }
  68. // Upstream retrieves the configured upstream server URL for the provided
  69. // schemes. If no configured upstream server matches, a *NoUpstreamError is
  70. // returned. An empty URL.Scheme means that the caller needs to perform
  71. // auto-discovery with URL.Host.
  72. func (s *Server) Upstream(schemes ...string) (*url.URL, error) {
  73. var urls []*url.URL
  74. for _, scheme := range append(schemes, "") {
  75. u, ok := s.upstreams[scheme]
  76. if ok {
  77. urls = append(urls, u)
  78. }
  79. }
  80. if len(urls) == 0 {
  81. return nil, &NoUpstreamError{schemes}
  82. }
  83. if len(urls) > 1 {
  84. return nil, fmt.Errorf("multiple upstream servers are configured for schemes %v", schemes)
  85. }
  86. return urls[0], nil
  87. }
  88. func (s *Server) parseIMAPUpstream() error {
  89. u, err := s.Upstream("imap", "imaps", "imap+insecure")
  90. if err != nil {
  91. return fmt.Errorf("failed to parse upstream IMAP server: %v", err)
  92. }
  93. if u.Scheme == "" {
  94. u, err = discoverIMAP(u.Host)
  95. if err != nil {
  96. return fmt.Errorf("failed to discover IMAP server: %v", err)
  97. }
  98. }
  99. s.imap.host = u.Host
  100. switch u.Scheme {
  101. case "imap":
  102. // This space is intentionally left blank
  103. case "imaps":
  104. s.imap.tls = true
  105. case "imap+insecure":
  106. s.imap.insecure = true
  107. default:
  108. panic("unreachable")
  109. }
  110. c, err := s.dialIMAP()
  111. if err != nil {
  112. return fmt.Errorf("failed to connect to IMAP server: %v", err)
  113. }
  114. c.Close()
  115. s.e.Logger.Printf("Configured upstream IMAP server: %v", u)
  116. return nil
  117. }
  118. func (s *Server) parseSMTPUpstream() error {
  119. u, err := s.Upstream("smtp", "smtps", "smtp+insecure")
  120. if _, ok := err.(*NoUpstreamError); ok {
  121. return nil
  122. } else if err != nil {
  123. return fmt.Errorf("failed to parse upstream SMTP server: %v", err)
  124. }
  125. if u.Scheme == "" {
  126. u, err = discoverSMTP(u.Host)
  127. if err != nil {
  128. s.e.Logger.Printf("Failed to discover SMTP server: %v", err)
  129. return nil
  130. }
  131. }
  132. s.smtp.host = u.Host
  133. switch u.Scheme {
  134. case "smtp":
  135. // This space is intentionally left blank
  136. case "smtps":
  137. s.smtp.tls = true
  138. case "smtp+insecure":
  139. s.smtp.insecure = true
  140. default:
  141. panic("unreachable")
  142. }
  143. c, err := s.dialSMTP()
  144. if err != nil {
  145. return fmt.Errorf("failed to connect to SMTP server: %v", err)
  146. }
  147. c.Close()
  148. s.e.Logger.Printf("Configured upstream SMTP server: %v", u)
  149. return nil
  150. }
  151. func (s *Server) load() error {
  152. plugins := append([]Plugin(nil), plugins...)
  153. for _, p := range plugins {
  154. s.e.Logger.Printf("Registered plugin '%v'", p.Name())
  155. }
  156. luaPlugins, err := loadAllLuaPlugins(s.e.Logger)
  157. if err != nil {
  158. return fmt.Errorf("failed to load plugins: %v", err)
  159. }
  160. plugins = append(plugins, luaPlugins...)
  161. renderer := newRenderer(s.e.Logger, s.defaultTheme)
  162. if err := renderer.Load(plugins); err != nil {
  163. return fmt.Errorf("failed to load templates: %v", err)
  164. }
  165. // Once we've loaded plugins and templates from disk (which can take time),
  166. // swap them in the Server struct
  167. s.mutex.Lock()
  168. defer s.mutex.Unlock()
  169. // Close previous Lua plugins
  170. for _, p := range s.luaPlugins {
  171. if err := p.Close(); err != nil {
  172. s.e.Logger.Printf("Failed to unload plugin '%v': %v", p.Name(), err)
  173. }
  174. }
  175. s.plugins = plugins
  176. s.luaPlugins = luaPlugins
  177. s.e.Renderer = renderer
  178. for _, p := range plugins {
  179. p.SetRoutes(s.e.Group(""))
  180. }
  181. return nil
  182. }
  183. // Reload loads Lua plugins and templates from disk.
  184. func (s *Server) Reload() error {
  185. s.e.Logger.Printf("Reloading server")
  186. return s.load()
  187. }
  188. // Context is the context used by HTTP handlers.
  189. //
  190. // Use a type assertion to get it from a echo.Context:
  191. //
  192. // ctx := ectx.(*koushin.Context)
  193. type Context struct {
  194. echo.Context
  195. Server *Server
  196. Session *Session // nil if user isn't logged in
  197. }
  198. var aLongTimeAgo = time.Unix(233431200, 0)
  199. // SetSession sets a cookie for the provided session. Passing a nil session
  200. // unsets the cookie.
  201. func (ctx *Context) SetSession(s *Session) {
  202. cookie := http.Cookie{
  203. Name: cookieName,
  204. HttpOnly: true,
  205. // TODO: domain, secure
  206. }
  207. if s != nil {
  208. cookie.Value = s.token
  209. } else {
  210. cookie.Expires = aLongTimeAgo // unset the cookie
  211. }
  212. ctx.SetCookie(&cookie)
  213. }
  214. func isPublic(path string) bool {
  215. if strings.HasPrefix(path, "/plugins/") {
  216. parts := strings.Split(path, "/")
  217. return len(parts) >= 4 && parts[3] == "assets"
  218. }
  219. return path == "/login" || strings.HasPrefix(path, "/themes/")
  220. }
  221. func redirectToLogin(ctx *Context) error {
  222. path := ctx.Request().URL.Path
  223. to := "/login"
  224. if path != "/" && path != "/login" {
  225. to += "?next=" + url.QueryEscape(ctx.Request().URL.String())
  226. }
  227. return ctx.Redirect(http.StatusFound, to)
  228. }
  229. func handleUnauthenticated(next echo.HandlerFunc, ctx *Context) error {
  230. // Require auth for all requests except /login and assets
  231. if isPublic(ctx.Request().URL.Path) {
  232. return next(ctx)
  233. } else {
  234. return redirectToLogin(ctx)
  235. }
  236. }
  237. type Options struct {
  238. Upstreams []string
  239. Theme string
  240. }
  241. // New creates a new server.
  242. func New(e *echo.Echo, options *Options) (*Server, error) {
  243. s, err := newServer(e, options)
  244. if err != nil {
  245. return nil, err
  246. }
  247. if err := s.load(); err != nil {
  248. return nil, err
  249. }
  250. e.HTTPErrorHandler = func(err error, c echo.Context) {
  251. code := http.StatusInternalServerError
  252. if he, ok := err.(*echo.HTTPError); ok {
  253. code = he.Code
  254. } else {
  255. c.Logger().Error(err)
  256. }
  257. // TODO: hide internal errors
  258. c.String(code, err.Error())
  259. }
  260. e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
  261. return func(ectx echo.Context) error {
  262. s.mutex.RLock()
  263. err := next(ectx)
  264. s.mutex.RUnlock()
  265. return err
  266. }
  267. })
  268. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  269. return func(ectx echo.Context) error {
  270. // `style-src 'unsafe-inline'` is required for e-mails with
  271. // embedded stylesheets
  272. ectx.Response().Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'")
  273. // DNS prefetching has privacy implications
  274. ectx.Response().Header().Set("X-DNS-Prefetch-Control", "off")
  275. return next(ectx)
  276. }
  277. })
  278. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  279. return func(ectx echo.Context) error {
  280. ctx := &Context{Context: ectx, Server: s}
  281. ctx.Set("context", ctx)
  282. cookie, err := ctx.Cookie(cookieName)
  283. if err == http.ErrNoCookie {
  284. return handleUnauthenticated(next, ctx)
  285. } else if err != nil {
  286. return err
  287. }
  288. ctx.Session, err = ctx.Server.Sessions.get(cookie.Value)
  289. if err == errSessionExpired {
  290. ctx.SetSession(nil)
  291. return handleUnauthenticated(next, ctx)
  292. } else if err != nil {
  293. return err
  294. }
  295. ctx.Session.ping()
  296. return next(ctx)
  297. }
  298. })
  299. e.Static("/themes", "themes")
  300. return s, nil
  301. }