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.
 
 
 
 

173 regels
3.1 KiB

  1. package koushin
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "time"
  7. "github.com/labstack/echo/v4"
  8. imapclient "github.com/emersion/go-imap/client"
  9. )
  10. const cookieName = "koushin_session"
  11. type Server struct {
  12. imap struct {
  13. host string
  14. tls bool
  15. insecure bool
  16. pool *ConnPool
  17. }
  18. }
  19. func NewServer(imapURL string) (*Server, error) {
  20. u, err := url.Parse(imapURL)
  21. if err != nil {
  22. return nil, err
  23. }
  24. s := &Server{}
  25. s.imap.host = u.Host
  26. switch u.Scheme {
  27. case "imap":
  28. // This space is intentionally left blank
  29. case "imaps":
  30. s.imap.tls = true
  31. case "imap+insecure":
  32. s.imap.insecure = true
  33. default:
  34. return nil, fmt.Errorf("unrecognized IMAP URL scheme: %s", u.Scheme)
  35. }
  36. s.imap.pool = NewConnPool()
  37. return s, nil
  38. }
  39. func (s *Server) connectIMAP() (*imapclient.Client, error) {
  40. var c *imapclient.Client
  41. var err error
  42. if s.imap.tls {
  43. c, err = imapclient.DialTLS(s.imap.host, nil)
  44. if err != nil {
  45. return nil, err
  46. }
  47. } else {
  48. c, err = imapclient.Dial(s.imap.host)
  49. if err != nil {
  50. return nil, err
  51. }
  52. if !s.imap.insecure {
  53. if err := c.StartTLS(nil); err != nil {
  54. c.Close()
  55. return nil, err
  56. }
  57. }
  58. }
  59. return c, err
  60. }
  61. type context struct {
  62. echo.Context
  63. server *Server
  64. conn *imapclient.Client
  65. }
  66. var aLongTimeAgo = time.Unix(233431200, 0)
  67. func (c *context) setToken(token string) {
  68. cookie := http.Cookie{
  69. Name: cookieName,
  70. Value: token,
  71. HttpOnly: true,
  72. // TODO: domain, secure
  73. }
  74. if token == "" {
  75. cookie.Expires = aLongTimeAgo // unset the cookie
  76. }
  77. c.SetCookie(&cookie)
  78. }
  79. func handleLogin(ectx echo.Context) error {
  80. ctx := ectx.(*context)
  81. username := ctx.FormValue("username")
  82. password := ctx.FormValue("password")
  83. if username != "" && password != "" {
  84. conn, err := ctx.server.connectIMAP()
  85. if err != nil {
  86. return err
  87. }
  88. if err := conn.Login(username, password); err != nil {
  89. conn.Logout()
  90. return ctx.Render(http.StatusOK, "login.html", nil)
  91. }
  92. token, err := ctx.server.imap.pool.Put(conn)
  93. if err != nil {
  94. return err
  95. }
  96. ctx.setToken(token)
  97. return ctx.Redirect(http.StatusFound, "/")
  98. }
  99. return ctx.Render(http.StatusOK, "login.html", nil)
  100. }
  101. func New(imapURL string) *echo.Echo {
  102. e := echo.New()
  103. s, err := NewServer(imapURL)
  104. if err != nil {
  105. e.Logger.Fatal(err)
  106. }
  107. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  108. return func(ectx echo.Context) error {
  109. ctx := &context{Context: ectx, server: s}
  110. cookie, err := ctx.Cookie(cookieName)
  111. if err == http.ErrNoCookie {
  112. return next(ctx)
  113. } else if err != nil {
  114. return err
  115. }
  116. ctx.conn, err = ctx.server.imap.pool.Get(cookie.Value)
  117. if err == ErrSessionExpired {
  118. ctx.setToken("")
  119. return ctx.Redirect(http.StatusFound, "/login")
  120. } else if err != nil {
  121. return err
  122. }
  123. return next(ctx)
  124. }
  125. })
  126. e.Renderer, err = loadTemplates()
  127. if err != nil {
  128. e.Logger.Fatal("Failed to load templates:", err)
  129. }
  130. e.GET("/", func(ectx echo.Context) error {
  131. ctx := ectx.(*context)
  132. if ctx.conn == nil {
  133. return ctx.Redirect(http.StatusFound, "/login")
  134. }
  135. return ctx.Render(http.StatusOK, "index.html", nil)
  136. })
  137. e.GET("/login", handleLogin)
  138. e.POST("/login", handleLogin)
  139. e.Static("/assets", "public/assets")
  140. return e
  141. }