A webmail client. Forked from https://git.sr.ht/~migadu/alps
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

276 wiersze
6.7 KiB

  1. package koushin
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "mime"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "github.com/emersion/go-imap"
  11. imapclient "github.com/emersion/go-imap/client"
  12. "github.com/emersion/go-message"
  13. "github.com/emersion/go-sasl"
  14. "github.com/labstack/echo/v4"
  15. )
  16. func handleGetMailbox(ectx echo.Context) error {
  17. ctx := ectx.(*context)
  18. mboxName, err := url.PathUnescape(ctx.Param("mbox"))
  19. if err != nil {
  20. return echo.NewHTTPError(http.StatusBadRequest, err)
  21. }
  22. page := 0
  23. if pageStr := ctx.QueryParam("page"); pageStr != "" {
  24. var err error
  25. if page, err = strconv.Atoi(pageStr); err != nil || page < 0 {
  26. return echo.NewHTTPError(http.StatusBadRequest, "invalid page index")
  27. }
  28. }
  29. var mailboxes []*imap.MailboxInfo
  30. var msgs []imapMessage
  31. var mbox *imap.MailboxStatus
  32. err = ctx.session.Do(func(c *imapclient.Client) error {
  33. var err error
  34. if mailboxes, err = listMailboxes(c); err != nil {
  35. return err
  36. }
  37. if msgs, err = listMessages(c, mboxName, page); err != nil {
  38. return err
  39. }
  40. mbox = c.Mailbox()
  41. return nil
  42. })
  43. if err != nil {
  44. return err
  45. }
  46. prevPage, nextPage := -1, -1
  47. if page > 0 {
  48. prevPage = page - 1
  49. }
  50. if (page+1)*messagesPerPage < int(mbox.Messages) {
  51. nextPage = page + 1
  52. }
  53. return ctx.Render(http.StatusOK, "mailbox.html", map[string]interface{}{
  54. "Mailbox": mbox,
  55. "Mailboxes": mailboxes,
  56. "Messages": msgs,
  57. "PrevPage": prevPage,
  58. "NextPage": nextPage,
  59. })
  60. }
  61. func handleLogin(ectx echo.Context) error {
  62. ctx := ectx.(*context)
  63. username := ctx.FormValue("username")
  64. password := ctx.FormValue("password")
  65. if username != "" && password != "" {
  66. conn, err := ctx.server.connectIMAP()
  67. if err != nil {
  68. return err
  69. }
  70. if err := conn.Login(username, password); err != nil {
  71. conn.Logout()
  72. return ctx.Render(http.StatusOK, "login.html", nil)
  73. }
  74. token, err := ctx.server.imap.pool.Put(conn, username, password)
  75. if err != nil {
  76. return fmt.Errorf("failed to put connection in pool: %v", err)
  77. }
  78. ctx.setToken(token)
  79. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  80. }
  81. return ctx.Render(http.StatusOK, "login.html", nil)
  82. }
  83. func handleLogout(ectx echo.Context) error {
  84. ctx := ectx.(*context)
  85. err := ctx.session.Do(func(c *imapclient.Client) error {
  86. return c.Logout()
  87. })
  88. if err != nil {
  89. return fmt.Errorf("failed to logout: %v", err)
  90. }
  91. ctx.setToken("")
  92. return ctx.Redirect(http.StatusFound, "/login")
  93. }
  94. func handleGetPart(ctx *context, raw bool) error {
  95. mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
  96. if err != nil {
  97. return echo.NewHTTPError(http.StatusBadRequest, err)
  98. }
  99. partPathString := ctx.QueryParam("part")
  100. partPath, err := parsePartPath(partPathString)
  101. if err != nil {
  102. return echo.NewHTTPError(http.StatusBadRequest, err)
  103. }
  104. var msg *imapMessage
  105. var part *message.Entity
  106. var mbox *imap.MailboxStatus
  107. err = ctx.session.Do(func(c *imapclient.Client) error {
  108. var err error
  109. msg, part, err = getMessagePart(c, mboxName, uid, partPath)
  110. mbox = c.Mailbox()
  111. return err
  112. })
  113. if err != nil {
  114. return err
  115. }
  116. mimeType, _, err := part.Header.ContentType()
  117. if err != nil {
  118. return fmt.Errorf("failed to parse part Content-Type: %v", err)
  119. }
  120. if len(partPath) == 0 {
  121. mimeType = "message/rfc822"
  122. }
  123. if raw {
  124. disp, dispParams, _ := part.Header.ContentDisposition()
  125. filename := dispParams["filename"]
  126. // TODO: set Content-Length if possible
  127. if !strings.EqualFold(mimeType, "text/plain") || strings.EqualFold(disp, "attachment") {
  128. dispParams := make(map[string]string)
  129. if filename != "" {
  130. dispParams["filename"] = filename
  131. }
  132. disp := mime.FormatMediaType("attachment", dispParams)
  133. ctx.Response().Header().Set("Content-Disposition", disp)
  134. }
  135. return ctx.Stream(http.StatusOK, mimeType, part.Body)
  136. }
  137. var body string
  138. if strings.HasPrefix(strings.ToLower(mimeType), "text/") {
  139. b, err := ioutil.ReadAll(part.Body)
  140. if err != nil {
  141. return fmt.Errorf("failed to read part body: %v", err)
  142. }
  143. body = string(b)
  144. }
  145. return ctx.Render(http.StatusOK, "message.html", map[string]interface{}{
  146. "Mailbox": mbox,
  147. "Message": msg,
  148. "Body": body,
  149. "PartPath": partPathString,
  150. "MailboxPage": (mbox.Messages - msg.SeqNum) / messagesPerPage,
  151. })
  152. }
  153. func handleCompose(ectx echo.Context) error {
  154. ctx := ectx.(*context)
  155. var msg OutgoingMessage
  156. if strings.ContainsRune(ctx.session.username, '@') {
  157. msg.From = ctx.session.username
  158. }
  159. if ctx.Request().Method == http.MethodGet && ctx.Param("uid") != "" {
  160. // This is a reply
  161. mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
  162. if err != nil {
  163. return echo.NewHTTPError(http.StatusBadRequest, err)
  164. }
  165. partPath, err := parsePartPath(ctx.QueryParam("part"))
  166. if err != nil {
  167. return echo.NewHTTPError(http.StatusBadRequest, err)
  168. }
  169. var inReplyTo *imapMessage
  170. var part *message.Entity
  171. err = ctx.session.Do(func(c *imapclient.Client) error {
  172. var err error
  173. inReplyTo, part, err = getMessagePart(c, mboxName, uid, partPath)
  174. return err
  175. })
  176. if err != nil {
  177. return err
  178. }
  179. mimeType, _, err := part.Header.ContentType()
  180. if err != nil {
  181. return fmt.Errorf("failed to parse part Content-Type: %v", err)
  182. }
  183. if !strings.HasPrefix(strings.ToLower(mimeType), "text/") {
  184. err := fmt.Errorf("cannot reply to \"%v\" part", mimeType)
  185. return echo.NewHTTPError(http.StatusBadRequest, err)
  186. }
  187. msg.Text, err = quote(part.Body)
  188. if err != nil {
  189. return err
  190. }
  191. msg.InReplyTo = inReplyTo.Envelope.MessageId
  192. // TODO: populate From from known user addresses and inReplyTo.Envelope.To
  193. replyTo := inReplyTo.Envelope.ReplyTo
  194. if len(replyTo) == 0 {
  195. replyTo = inReplyTo.Envelope.From
  196. }
  197. if len(replyTo) > 0 {
  198. msg.To = make([]string, len(replyTo))
  199. for i, to := range replyTo {
  200. msg.To[i] = to.Address()
  201. }
  202. }
  203. msg.Subject = inReplyTo.Envelope.Subject
  204. if !strings.HasPrefix(strings.ToLower(msg.Subject), "re:") {
  205. msg.Subject = "Re: " + msg.Subject
  206. }
  207. }
  208. if ctx.Request().Method == http.MethodPost {
  209. msg.From = ctx.FormValue("from")
  210. msg.To = parseAddressList(ctx.FormValue("to"))
  211. msg.Subject = ctx.FormValue("subject")
  212. msg.Text = ctx.FormValue("text")
  213. msg.InReplyTo = ctx.FormValue("in_reply_to")
  214. c, err := ctx.server.connectSMTP()
  215. if err != nil {
  216. return err
  217. }
  218. defer c.Close()
  219. auth := sasl.NewPlainClient("", ctx.session.username, ctx.session.password)
  220. if err := c.Auth(auth); err != nil {
  221. return echo.NewHTTPError(http.StatusForbidden, err)
  222. }
  223. if err := sendMessage(c, &msg); err != nil {
  224. return err
  225. }
  226. if err := c.Quit(); err != nil {
  227. return fmt.Errorf("QUIT failed: %v", err)
  228. }
  229. // TODO: append to IMAP Sent mailbox
  230. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  231. }
  232. return ctx.Render(http.StatusOK, "compose.html", map[string]interface{}{
  233. "Message": &msg,
  234. })
  235. }