A webmail client. Forked from https://git.sr.ht/~migadu/alps
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

254 líneas
5.3 KiB

  1. package koushin
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "mime"
  6. "net/http"
  7. "net/url"
  8. "strings"
  9. "time"
  10. imapclient "github.com/emersion/go-imap/client"
  11. "github.com/labstack/echo/v4"
  12. )
  13. const cookieName = "koushin_session"
  14. type Server struct {
  15. imap struct {
  16. host string
  17. tls bool
  18. insecure bool
  19. pool *ConnPool
  20. }
  21. }
  22. func NewServer(imapURL string) (*Server, error) {
  23. u, err := url.Parse(imapURL)
  24. if err != nil {
  25. return nil, err
  26. }
  27. s := &Server{}
  28. s.imap.host = u.Host
  29. switch u.Scheme {
  30. case "imap":
  31. // This space is intentionally left blank
  32. case "imaps":
  33. s.imap.tls = true
  34. case "imap+insecure":
  35. s.imap.insecure = true
  36. default:
  37. return nil, fmt.Errorf("unrecognized IMAP URL scheme: %s", u.Scheme)
  38. }
  39. s.imap.pool = NewConnPool()
  40. return s, nil
  41. }
  42. type context struct {
  43. echo.Context
  44. server *Server
  45. conn *imapclient.Client
  46. }
  47. var aLongTimeAgo = time.Unix(233431200, 0)
  48. func (c *context) setToken(token string) {
  49. cookie := http.Cookie{
  50. Name: cookieName,
  51. Value: token,
  52. HttpOnly: true,
  53. // TODO: domain, secure
  54. }
  55. if token == "" {
  56. cookie.Expires = aLongTimeAgo // unset the cookie
  57. }
  58. c.SetCookie(&cookie)
  59. }
  60. func handleLogin(ectx echo.Context) error {
  61. ctx := ectx.(*context)
  62. username := ctx.FormValue("username")
  63. password := ctx.FormValue("password")
  64. if username != "" && password != "" {
  65. conn, err := ctx.server.connectIMAP()
  66. if err != nil {
  67. return err
  68. }
  69. if err := conn.Login(username, password); err != nil {
  70. conn.Logout()
  71. return ctx.Render(http.StatusOK, "login.html", nil)
  72. }
  73. token, err := ctx.server.imap.pool.Put(conn)
  74. if err != nil {
  75. return err
  76. }
  77. ctx.setToken(token)
  78. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  79. }
  80. return ctx.Render(http.StatusOK, "login.html", nil)
  81. }
  82. func handleGetPart(ctx *context, raw bool) error {
  83. mboxName := ctx.Param("mbox")
  84. uid, err := parseUid(ctx.Param("uid"))
  85. if err != nil {
  86. return echo.NewHTTPError(http.StatusBadRequest, err)
  87. }
  88. partPathString := ctx.QueryParam("part")
  89. partPath, err := parsePartPath(partPathString)
  90. if err != nil {
  91. return echo.NewHTTPError(http.StatusBadRequest, err)
  92. }
  93. msg, part, err := getMessagePart(ctx.conn, mboxName, uid, partPath)
  94. if err != nil {
  95. return err
  96. }
  97. mimeType, _, err := part.Header.ContentType()
  98. if err != nil {
  99. return err
  100. }
  101. if len(partPath) == 0 {
  102. mimeType = "message/rfc822"
  103. }
  104. if raw {
  105. disp, dispParams, _ := part.Header.ContentDisposition()
  106. filename := dispParams["filename"]
  107. if !strings.EqualFold(mimeType, "text/plain") || strings.EqualFold(disp, "attachment") {
  108. dispParams := make(map[string]string)
  109. if filename != "" {
  110. dispParams["filename"] = filename
  111. }
  112. disp := mime.FormatMediaType("attachment", dispParams)
  113. ctx.Response().Header().Set("Content-Disposition", disp)
  114. }
  115. return ctx.Stream(http.StatusOK, mimeType, part.Body)
  116. }
  117. var body string
  118. if strings.HasPrefix(strings.ToLower(mimeType), "text/") {
  119. b, err := ioutil.ReadAll(part.Body)
  120. if err != nil {
  121. return err
  122. }
  123. body = string(b)
  124. }
  125. return ctx.Render(http.StatusOK, "message.html", map[string]interface{}{
  126. "Mailbox": ctx.conn.Mailbox(),
  127. "Message": msg,
  128. "Body": body,
  129. "PartPath": partPathString,
  130. })
  131. }
  132. func New(imapURL string) *echo.Echo {
  133. e := echo.New()
  134. s, err := NewServer(imapURL)
  135. if err != nil {
  136. e.Logger.Fatal(err)
  137. }
  138. e.HTTPErrorHandler = func(err error, c echo.Context) {
  139. code := http.StatusInternalServerError
  140. if he, ok := err.(*echo.HTTPError); ok {
  141. code = he.Code
  142. } else {
  143. c.Logger().Error(err)
  144. }
  145. // TODO: hide internal errors
  146. c.String(code, err.Error())
  147. }
  148. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  149. return func(ectx echo.Context) error {
  150. ctx := &context{Context: ectx, server: s}
  151. cookie, err := ctx.Cookie(cookieName)
  152. if err == http.ErrNoCookie {
  153. // Require auth for all pages except /login
  154. if ctx.Path() == "/login" {
  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.conn, err = ctx.server.imap.pool.Get(cookie.Value)
  163. if err == ErrSessionExpired {
  164. ctx.setToken("")
  165. return ctx.Redirect(http.StatusFound, "/login")
  166. } else if err != nil {
  167. return err
  168. }
  169. return next(ctx)
  170. }
  171. })
  172. e.Renderer, err = loadTemplates()
  173. if err != nil {
  174. e.Logger.Fatal("Failed to load templates:", err)
  175. }
  176. e.GET("/mailbox/:mbox", func(ectx echo.Context) error {
  177. ctx := ectx.(*context)
  178. mailboxes, err := listMailboxes(ctx.conn)
  179. if err != nil {
  180. return err
  181. }
  182. msgs, err := listMessages(ctx.conn, ctx.Param("mbox"))
  183. if err != nil {
  184. return err
  185. }
  186. return ctx.Render(http.StatusOK, "mailbox.html", map[string]interface{}{
  187. "Mailbox": ctx.conn.Mailbox(),
  188. "Mailboxes": mailboxes,
  189. "Messages": msgs,
  190. })
  191. })
  192. e.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
  193. ctx := ectx.(*context)
  194. return handleGetPart(ctx, false)
  195. })
  196. e.GET("/message/:mbox/:uid/raw", func(ectx echo.Context) error {
  197. ctx := ectx.(*context)
  198. return handleGetPart(ctx, true)
  199. })
  200. e.GET("/login", handleLogin)
  201. e.POST("/login", handleLogin)
  202. e.GET("/logout", func(ectx echo.Context) error {
  203. ctx := ectx.(*context)
  204. if err := ctx.conn.Logout(); err != nil {
  205. return err
  206. }
  207. ctx.setToken("")
  208. return ctx.Redirect(http.StatusFound, "/login")
  209. })
  210. e.Static("/assets", "public/assets")
  211. return e
  212. }