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.
 
 
 
 

1280 lines
33 KiB

  1. package alpsbase
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "mime"
  8. "net/http"
  9. "net/textproto"
  10. "net/url"
  11. "strconv"
  12. "strings"
  13. "git.sr.ht/~migadu/alps"
  14. "github.com/emersion/go-imap"
  15. "github.com/emersion/go-message"
  16. "github.com/emersion/go-message/mail"
  17. "github.com/emersion/go-smtp"
  18. "github.com/labstack/echo/v4"
  19. "jaytaylor.com/html2text"
  20. imapclient "github.com/emersion/go-imap/client"
  21. imapmove "github.com/emersion/go-imap-move"
  22. )
  23. func registerRoutes(p *alps.GoPlugin) {
  24. p.GET("/", func(ctx *alps.Context) error {
  25. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  26. })
  27. p.GET("/mailbox/:mbox", handleGetMailbox)
  28. p.POST("/mailbox/:mbox", handleGetMailbox)
  29. p.GET("/new-mailbox", handleNewMailbox)
  30. p.POST("/new-mailbox", handleNewMailbox)
  31. p.GET("/delete-mailbox/:mbox", handleDeleteMailbox)
  32. p.POST("/delete-mailbox/:mbox", handleDeleteMailbox)
  33. p.GET("/message/:mbox/:uid", func(ctx *alps.Context) error {
  34. return handleGetPart(ctx, false)
  35. })
  36. p.GET("/message/:mbox/:uid/raw", func(ctx *alps.Context) error {
  37. return handleGetPart(ctx, true)
  38. })
  39. p.GET("/login", handleLogin)
  40. p.POST("/login", handleLogin)
  41. p.GET("/logout", handleLogout)
  42. p.GET("/compose", handleComposeNew)
  43. p.POST("/compose", handleComposeNew)
  44. p.POST("/compose/attachment", handleComposeAttachment)
  45. p.POST("/compose/attachment/:uuid/remove", handleCancelAttachment)
  46. p.GET("/message/:mbox/:uid/reply", handleReply)
  47. p.POST("/message/:mbox/:uid/reply", handleReply)
  48. p.GET("/message/:mbox/:uid/forward", handleForward)
  49. p.POST("/message/:mbox/:uid/forward", handleForward)
  50. p.GET("/message/:mbox/:uid/edit", handleEdit)
  51. p.POST("/message/:mbox/:uid/edit", handleEdit)
  52. p.POST("/message/:mbox/move", handleMove)
  53. p.POST("/message/:mbox/delete", handleDelete)
  54. p.POST("/message/:mbox/flag", handleSetFlags)
  55. p.GET("/settings", handleSettings)
  56. p.POST("/settings", handleSettings)
  57. }
  58. type IMAPBaseRenderData struct {
  59. alps.BaseRenderData
  60. CategorizedMailboxes CategorizedMailboxes
  61. Mailboxes []MailboxInfo
  62. Mailbox *MailboxStatus
  63. Inbox *MailboxStatus
  64. Subscriptions map[string]*MailboxStatus
  65. }
  66. type MailboxRenderData struct {
  67. IMAPBaseRenderData
  68. Messages []IMAPMessage
  69. PrevPage, NextPage int
  70. Query string
  71. }
  72. type MailboxDetails struct {
  73. Info *MailboxInfo
  74. Status *MailboxStatus
  75. }
  76. // Organizes mailboxes into common/uncommon categories
  77. type CategorizedMailboxes struct {
  78. Common struct {
  79. Inbox MailboxDetails
  80. Drafts MailboxDetails
  81. Sent MailboxDetails
  82. Junk MailboxDetails
  83. Trash MailboxDetails
  84. Archive MailboxDetails
  85. }
  86. Additional []MailboxDetails
  87. }
  88. func newIMAPBaseRenderData(ctx *alps.Context,
  89. base *alps.BaseRenderData) (*IMAPBaseRenderData, error) {
  90. mboxName, err := url.PathUnescape(ctx.Param("mbox"))
  91. if err != nil {
  92. return nil, echo.NewHTTPError(http.StatusBadRequest, err)
  93. }
  94. settings, err := loadSettings(ctx.Session.Store())
  95. if err != nil {
  96. return nil, fmt.Errorf("failed to load settings: %v", err)
  97. }
  98. subscriptions := make(map[string]*MailboxStatus)
  99. var mailboxes []MailboxInfo
  100. var active, inbox *MailboxStatus
  101. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  102. var err error
  103. if mailboxes, err = listMailboxes(c); err != nil {
  104. return err
  105. }
  106. if mboxName != "" {
  107. if active, err = getMailboxStatus(c, mboxName); err != nil {
  108. return echo.NewHTTPError(http.StatusNotFound, err)
  109. }
  110. }
  111. if mboxName == "INBOX" {
  112. inbox = active
  113. } else {
  114. if inbox, err = getMailboxStatus(c, "INBOX"); err != nil {
  115. return err
  116. }
  117. }
  118. for _, sub := range settings.Subscriptions {
  119. if status, err := getMailboxStatus(c, sub); err != nil {
  120. return err
  121. } else {
  122. subscriptions[sub] = status
  123. }
  124. }
  125. return nil
  126. })
  127. if err != nil {
  128. return nil, err
  129. }
  130. var categorized CategorizedMailboxes
  131. mmap := map[string]*MailboxDetails{
  132. "INBOX": &categorized.Common.Inbox,
  133. "Drafts": &categorized.Common.Drafts,
  134. "Sent": &categorized.Common.Sent,
  135. "Junk": &categorized.Common.Junk,
  136. "Trash": &categorized.Common.Trash,
  137. "Archive": &categorized.Common.Archive,
  138. }
  139. for i, _ := range mailboxes {
  140. // Populate unseen & active states
  141. if active != nil && mailboxes[i].Name == active.Name {
  142. mailboxes[i].Unseen = int(active.Unseen)
  143. mailboxes[i].Total = int(active.Messages)
  144. mailboxes[i].Active = true
  145. }
  146. if mailboxes[i].Name == inbox.Name {
  147. mailboxes[i].Unseen = int(inbox.Unseen)
  148. mailboxes[i].Total = int(inbox.Messages)
  149. }
  150. status, _ := subscriptions[mailboxes[i].Name]
  151. if ptr, ok := mmap[mailboxes[i].Name]; ok {
  152. ptr.Info = &mailboxes[i]
  153. ptr.Status = status
  154. } else {
  155. categorized.Additional = append(categorized.Additional,
  156. MailboxDetails{
  157. Info: &mailboxes[i],
  158. Status: status,
  159. })
  160. }
  161. }
  162. return &IMAPBaseRenderData{
  163. BaseRenderData: *base,
  164. CategorizedMailboxes: categorized,
  165. Mailboxes: mailboxes,
  166. Inbox: inbox,
  167. Mailbox: active,
  168. Subscriptions: subscriptions,
  169. }, nil
  170. }
  171. func handleGetMailbox(ctx *alps.Context) error {
  172. ibase, err := newIMAPBaseRenderData(ctx, alps.NewBaseRenderData(ctx))
  173. if err != nil {
  174. return err
  175. }
  176. mbox := ibase.Mailbox
  177. title := mbox.Name
  178. if title == "INBOX" {
  179. title = "Inbox"
  180. }
  181. if mbox.Unseen > 0 {
  182. title = fmt.Sprintf("(%d) %s", mbox.Unseen, title)
  183. }
  184. ibase.BaseRenderData.WithTitle(title)
  185. page := 0
  186. if pageStr := ctx.QueryParam("page"); pageStr != "" {
  187. var err error
  188. if page, err = strconv.Atoi(pageStr); err != nil || page < 0 {
  189. return echo.NewHTTPError(http.StatusBadRequest, "invalid page index")
  190. }
  191. }
  192. settings, err := loadSettings(ctx.Session.Store())
  193. if err != nil {
  194. return err
  195. }
  196. messagesPerPage := settings.MessagesPerPage
  197. query := ctx.QueryParam("query")
  198. var (
  199. msgs []IMAPMessage
  200. total int
  201. )
  202. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  203. var err error
  204. if query != "" {
  205. msgs, total, err = searchMessages(c, mbox.Name, query, page, messagesPerPage)
  206. } else {
  207. msgs, err = listMessages(c, mbox, page, messagesPerPage)
  208. }
  209. if err != nil {
  210. return err
  211. }
  212. return nil
  213. })
  214. if err != nil {
  215. return err
  216. }
  217. prevPage, nextPage := -1, -1
  218. if query != "" {
  219. if page > 0 {
  220. prevPage = page - 1
  221. }
  222. if (page+1)*messagesPerPage <= total {
  223. nextPage = page + 1
  224. }
  225. } else {
  226. if page > 0 {
  227. prevPage = page - 1
  228. }
  229. if (page+1)*messagesPerPage < int(mbox.Messages) {
  230. nextPage = page + 1
  231. }
  232. }
  233. return ctx.Render(http.StatusOK, "mailbox.html", &MailboxRenderData{
  234. IMAPBaseRenderData: *ibase,
  235. Messages: msgs,
  236. PrevPage: prevPage,
  237. NextPage: nextPage,
  238. Query: query,
  239. })
  240. }
  241. type NewMailboxRenderData struct {
  242. IMAPBaseRenderData
  243. Error string
  244. }
  245. func handleNewMailbox(ctx *alps.Context) error {
  246. ibase, err := newIMAPBaseRenderData(ctx, alps.NewBaseRenderData(ctx))
  247. if err != nil {
  248. return err
  249. }
  250. ibase.BaseRenderData.WithTitle("Create new folder")
  251. if ctx.Request().Method == http.MethodPost {
  252. name := ctx.FormValue("name")
  253. if name == "" {
  254. return ctx.Render(http.StatusOK, "new-mailbox.html", &NewMailboxRenderData{
  255. IMAPBaseRenderData: *ibase,
  256. Error: "Name is required",
  257. })
  258. }
  259. err := ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  260. return c.Create(name)
  261. })
  262. if err != nil {
  263. return ctx.Render(http.StatusOK, "new-mailbox.html", &NewMailboxRenderData{
  264. IMAPBaseRenderData: *ibase,
  265. Error: err.Error(),
  266. })
  267. }
  268. return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%s", url.PathEscape(name)))
  269. }
  270. return ctx.Render(http.StatusOK, "new-mailbox.html", &NewMailboxRenderData{
  271. IMAPBaseRenderData: *ibase,
  272. Error: "",
  273. })
  274. }
  275. func handleDeleteMailbox(ctx *alps.Context) error {
  276. ibase, err := newIMAPBaseRenderData(ctx, alps.NewBaseRenderData(ctx))
  277. if err != nil {
  278. return err
  279. }
  280. mbox := ibase.Mailbox
  281. ibase.BaseRenderData.WithTitle("Delete folder '" + mbox.Name + "'")
  282. if ctx.Request().Method == http.MethodPost {
  283. ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  284. return c.Delete(mbox.Name)
  285. })
  286. ctx.Session.PutNotice("Mailbox deleted.")
  287. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  288. }
  289. return ctx.Render(http.StatusOK, "delete-mailbox.html", ibase)
  290. }
  291. func handleLogin(ctx *alps.Context) error {
  292. username := ctx.FormValue("username")
  293. password := ctx.FormValue("password")
  294. remember := ctx.FormValue("remember-me")
  295. renderData := struct {
  296. alps.BaseRenderData
  297. CanRememberMe bool
  298. }{
  299. BaseRenderData: *alps.NewBaseRenderData(ctx),
  300. CanRememberMe: ctx.Server.Options.LoginKey != nil,
  301. }
  302. if username == "" && password == "" {
  303. username, password = ctx.GetLoginToken()
  304. }
  305. if username != "" && password != "" {
  306. s, err := ctx.Server.Sessions.Put(username, password)
  307. if err != nil {
  308. if _, ok := err.(alps.AuthError); ok {
  309. return ctx.Render(http.StatusOK, "login.html", &renderData)
  310. }
  311. return fmt.Errorf("failed to put connection in pool: %v", err)
  312. }
  313. ctx.SetSession(s)
  314. if remember == "on" {
  315. ctx.SetLoginToken(username, password)
  316. }
  317. if path := ctx.QueryParam("next"); path != "" && path[0] == '/' && path != "/login" {
  318. return ctx.Redirect(http.StatusFound, path)
  319. }
  320. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  321. }
  322. return ctx.Render(http.StatusOK, "login.html", &renderData)
  323. }
  324. func handleLogout(ctx *alps.Context) error {
  325. ctx.Session.Close()
  326. ctx.SetSession(nil)
  327. ctx.SetLoginToken("", "")
  328. return ctx.Redirect(http.StatusFound, "/login")
  329. }
  330. type MessageRenderData struct {
  331. IMAPBaseRenderData
  332. Message *IMAPMessage
  333. Part *IMAPPartNode
  334. View interface{}
  335. MailboxPage int
  336. Flags map[string]bool
  337. }
  338. func handleGetPart(ctx *alps.Context, raw bool) error {
  339. _, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
  340. ibase, err := newIMAPBaseRenderData(ctx, alps.NewBaseRenderData(ctx))
  341. if err != nil {
  342. return err
  343. }
  344. mbox := ibase.Mailbox
  345. partPath, err := parsePartPath(ctx.QueryParam("part"))
  346. if err != nil {
  347. return echo.NewHTTPError(http.StatusBadRequest, err)
  348. }
  349. settings, err := loadSettings(ctx.Session.Store())
  350. if err != nil {
  351. return err
  352. }
  353. messagesPerPage := settings.MessagesPerPage
  354. var msg *IMAPMessage
  355. var part *message.Entity
  356. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  357. var err error
  358. if msg, part, err = getMessagePart(c, mbox.Name, uid, partPath); err != nil {
  359. return err
  360. }
  361. return nil
  362. })
  363. if err != nil {
  364. return err
  365. }
  366. mimeType, _, err := part.Header.ContentType()
  367. if err != nil {
  368. return fmt.Errorf("failed to parse part Content-Type: %v", err)
  369. }
  370. if len(partPath) == 0 {
  371. if ctx.QueryParam("plain") == "1" {
  372. mimeType = "text/plain"
  373. } else {
  374. mimeType = "message/rfc822"
  375. }
  376. }
  377. if raw {
  378. ctx.Response().Header().Set("Content-Type", mimeType)
  379. disp, dispParams, _ := part.Header.ContentDisposition()
  380. filename := dispParams["filename"]
  381. if len(partPath) == 0 {
  382. filename = msg.Envelope.Subject + ".eml"
  383. }
  384. // TODO: set Content-Length if possible
  385. // Be careful not to serve types like text/html as inline
  386. if !strings.EqualFold(mimeType, "text/plain") || strings.EqualFold(disp, "attachment") {
  387. dispParams := make(map[string]string)
  388. if filename != "" {
  389. dispParams["filename"] = filename
  390. }
  391. disp := mime.FormatMediaType("attachment", dispParams)
  392. ctx.Response().Header().Set("Content-Disposition", disp)
  393. }
  394. if len(partPath) == 0 {
  395. return part.WriteTo(ctx.Response())
  396. } else {
  397. return ctx.Stream(http.StatusOK, mimeType, part.Body)
  398. }
  399. }
  400. view, err := viewMessagePart(ctx, msg, part)
  401. if err == ErrViewUnsupported {
  402. view = nil
  403. }
  404. flags := make(map[string]bool)
  405. for _, f := range mbox.PermanentFlags {
  406. f = imap.CanonicalFlag(f)
  407. if f == imap.TryCreateFlag {
  408. continue
  409. }
  410. flags[f] = msg.HasFlag(f)
  411. }
  412. ibase.BaseRenderData.WithTitle(msg.Envelope.Subject)
  413. return ctx.Render(http.StatusOK, "message.html", &MessageRenderData{
  414. IMAPBaseRenderData: *ibase,
  415. Message: msg,
  416. Part: msg.PartByPath(partPath),
  417. View: view,
  418. MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage,
  419. Flags: flags,
  420. })
  421. }
  422. type ComposeRenderData struct {
  423. IMAPBaseRenderData
  424. Message *OutgoingMessage
  425. }
  426. type messagePath struct {
  427. Mailbox string
  428. Uid uint32
  429. }
  430. type composeOptions struct {
  431. Draft *messagePath
  432. Forward *messagePath
  433. InReplyTo *messagePath
  434. }
  435. // Send message, append it to the Sent mailbox, mark the original message as
  436. // answered
  437. func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOptions) error {
  438. err := ctx.Session.DoSMTP(func (c *smtp.Client) error {
  439. return sendMessage(c, msg)
  440. })
  441. if err != nil {
  442. if _, ok := err.(alps.AuthError); ok {
  443. return echo.NewHTTPError(http.StatusForbidden, err)
  444. }
  445. return fmt.Errorf("failed to send message: %v", err)
  446. }
  447. if inReplyTo := options.InReplyTo; inReplyTo != nil {
  448. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  449. return markMessageAnswered(c, inReplyTo.Mailbox, inReplyTo.Uid)
  450. })
  451. if err != nil {
  452. return fmt.Errorf("failed to mark original message as answered: %v", err)
  453. }
  454. }
  455. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  456. if _, err := appendMessage(c, msg, mailboxSent); err != nil {
  457. return err
  458. }
  459. if draft := options.Draft; draft != nil {
  460. if err := deleteMessage(c, draft.Mailbox, draft.Uid); err != nil {
  461. return err
  462. }
  463. }
  464. return nil
  465. })
  466. if err != nil {
  467. return fmt.Errorf("failed to save message to Sent mailbox: %v", err)
  468. }
  469. ctx.Session.PutNotice("Message sent.")
  470. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  471. }
  472. func handleCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOptions) error {
  473. ibase, err := newIMAPBaseRenderData(ctx, alps.NewBaseRenderData(ctx))
  474. if err != nil {
  475. return err
  476. }
  477. if msg.From == "" && strings.ContainsRune(ctx.Session.Username(), '@') {
  478. settings, err := loadSettings(ctx.Session.Store())
  479. if err != nil {
  480. return err
  481. }
  482. if settings.From != "" {
  483. addr := mail.Address{
  484. Name: settings.From,
  485. Address: ctx.Session.Username(),
  486. }
  487. msg.From = addr.String()
  488. } else {
  489. msg.From = ctx.Session.Username()
  490. }
  491. }
  492. if ctx.Request().Method == http.MethodPost {
  493. formParams, err := ctx.FormParams()
  494. if err != nil {
  495. return fmt.Errorf("failed to parse form: %v", err)
  496. }
  497. _, saveAsDraft := formParams["save_as_draft"]
  498. msg.From = ctx.FormValue("from")
  499. msg.To = parseAddressList(ctx.FormValue("to"))
  500. msg.Subject = ctx.FormValue("subject")
  501. msg.Text = ctx.FormValue("text")
  502. msg.InReplyTo = ctx.FormValue("in_reply_to")
  503. msg.MessageID = ctx.FormValue("message_id")
  504. form, err := ctx.MultipartForm()
  505. if err != nil {
  506. return fmt.Errorf("failed to get multipart form: %v", err)
  507. }
  508. // Fetch previous attachments from original message
  509. var original *messagePath
  510. if options.Draft != nil {
  511. original = options.Draft
  512. } else if options.Forward != nil {
  513. original = options.Forward
  514. }
  515. if original != nil {
  516. for _, s := range form.Value["prev_attachments"] {
  517. path, err := parsePartPath(s)
  518. if err != nil {
  519. return fmt.Errorf("failed to parse original attachment path: %v", err)
  520. }
  521. var part *message.Entity
  522. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  523. var err error
  524. _, part, err = getMessagePart(c, original.Mailbox, original.Uid, path)
  525. return err
  526. })
  527. if err != nil {
  528. return fmt.Errorf("failed to fetch attachment from original message: %v", err)
  529. }
  530. var buf bytes.Buffer
  531. if _, err := io.Copy(&buf, part.Body); err != nil {
  532. return fmt.Errorf("failed to copy attachment from original message: %v", err)
  533. }
  534. h := mail.AttachmentHeader{part.Header}
  535. mimeType, _, _ := h.ContentType()
  536. filename, _ := h.Filename()
  537. msg.Attachments = append(msg.Attachments, &imapAttachment{
  538. Mailbox: original.Mailbox,
  539. Uid: original.Uid,
  540. Node: &IMAPPartNode{
  541. Path: path,
  542. MIMEType: mimeType,
  543. Filename: filename,
  544. },
  545. Body: buf.Bytes(),
  546. })
  547. }
  548. } else if len(form.Value["prev_attachments"]) > 0 {
  549. return fmt.Errorf("previous attachments specified but no original message available")
  550. }
  551. for _, fh := range form.File["attachments"] {
  552. msg.Attachments = append(msg.Attachments, &formAttachment{fh})
  553. }
  554. uuids := ctx.FormValue("attachment-uuids")
  555. for _, uuid := range strings.Split(uuids, ",") {
  556. if uuid == "" {
  557. continue
  558. }
  559. attachment := ctx.Session.PopAttachment(uuid)
  560. if attachment == nil {
  561. return fmt.Errorf("Unable to retrieve message attachment %s from session", uuid)
  562. }
  563. msg.Attachments = append(msg.Attachments,
  564. &formAttachment{attachment.File})
  565. defer attachment.Form.RemoveAll()
  566. }
  567. if saveAsDraft {
  568. var (
  569. drafts *MailboxInfo
  570. uid uint32
  571. )
  572. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  573. drafts, err = appendMessage(c, msg, mailboxDrafts)
  574. if err != nil {
  575. return err
  576. }
  577. if draft := options.Draft; draft != nil {
  578. if err := deleteMessage(c, draft.Mailbox, draft.Uid); err != nil {
  579. return err
  580. }
  581. }
  582. criteria := &imap.SearchCriteria{
  583. Header: make(textproto.MIMEHeader),
  584. }
  585. criteria.Header.Add("Message-Id", msg.MessageID)
  586. if uids, err := c.UidSearch(criteria); err != nil {
  587. return err
  588. } else {
  589. if len(uids) != 1 {
  590. panic(fmt.Errorf("Duplicate message ID"))
  591. }
  592. uid = uids[0]
  593. }
  594. return nil
  595. })
  596. if err != nil {
  597. return fmt.Errorf("failed to save message to Draft mailbox: %v", err)
  598. }
  599. ctx.Session.PutNotice("Message saved as draft.")
  600. return ctx.Redirect(http.StatusFound, fmt.Sprintf(
  601. "/message/%s/%d/edit?part=1", drafts.Name, uid))
  602. } else {
  603. return submitCompose(ctx, msg, options)
  604. }
  605. }
  606. return ctx.Render(http.StatusOK, "compose.html", &ComposeRenderData{
  607. IMAPBaseRenderData: *ibase,
  608. Message: msg,
  609. })
  610. }
  611. func handleComposeNew(ctx *alps.Context) error {
  612. text := ctx.QueryParam("body")
  613. settings, err := loadSettings(ctx.Session.Store())
  614. if err != nil {
  615. return nil
  616. }
  617. if text == "" && settings.Signature != "" {
  618. text = "\n\n\n-- \n" + settings.Signature
  619. }
  620. // These are common mailto URL query parameters
  621. // TODO: cc, bcc
  622. var hdr mail.Header
  623. hdr.GenerateMessageID()
  624. mid, _ := hdr.MessageID()
  625. return handleCompose(ctx, &OutgoingMessage{
  626. To: strings.Split(ctx.QueryParam("to"), ","),
  627. Subject: ctx.QueryParam("subject"),
  628. MessageID: "<" + mid + ">",
  629. InReplyTo: ctx.QueryParam("in-reply-to"),
  630. Text: text,
  631. }, &composeOptions{})
  632. }
  633. func handleComposeAttachment(ctx *alps.Context) error {
  634. reader, err := ctx.Request().MultipartReader()
  635. if err != nil {
  636. return ctx.JSON(http.StatusBadRequest, map[string]string{
  637. "error": "Invalid request",
  638. })
  639. }
  640. form, err := reader.ReadForm(32 << 20) // 32 MB
  641. if err != nil {
  642. return ctx.JSON(http.StatusBadRequest, map[string]string{
  643. "error": "Invalid request",
  644. })
  645. }
  646. var uuids []string
  647. for _, fh := range form.File["attachments"] {
  648. uuid, err := ctx.Session.PutAttachment(fh, form)
  649. if err == alps.ErrAttachmentCacheSize {
  650. form.RemoveAll()
  651. return ctx.JSON(http.StatusBadRequest, map[string]string{
  652. "error": "Your attachments exceed the maximum file size. Remove some and try again.",
  653. })
  654. } else if err != nil {
  655. form.RemoveAll()
  656. ctx.Logger().Printf("PutAttachment: %v\n", err)
  657. return ctx.JSON(http.StatusBadRequest, map[string]string{
  658. "error": "failed to store attachment",
  659. })
  660. }
  661. uuids = append(uuids, uuid)
  662. }
  663. return ctx.JSON(http.StatusOK, &uuids)
  664. }
  665. func handleCancelAttachment(ctx *alps.Context) error {
  666. uuid := ctx.Param("uuid")
  667. a := ctx.Session.PopAttachment(uuid)
  668. if a != nil {
  669. a.Form.RemoveAll()
  670. }
  671. return ctx.JSON(http.StatusOK, nil)
  672. }
  673. func unwrapIMAPAddressList(addrs []*imap.Address) []string {
  674. l := make([]string, len(addrs))
  675. for i, addr := range addrs {
  676. l[i] = addr.Address()
  677. }
  678. return l
  679. }
  680. func handleReply(ctx *alps.Context) error {
  681. var inReplyToPath messagePath
  682. var err error
  683. inReplyToPath.Mailbox, inReplyToPath.Uid, err = parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
  684. if err != nil {
  685. return echo.NewHTTPError(http.StatusBadRequest, err)
  686. }
  687. var msg OutgoingMessage
  688. if ctx.Request().Method == http.MethodGet {
  689. // Populate fields from original message
  690. partPath, err := parsePartPath(ctx.QueryParam("part"))
  691. if err != nil {
  692. return echo.NewHTTPError(http.StatusBadRequest, err)
  693. }
  694. var inReplyTo *IMAPMessage
  695. var part *message.Entity
  696. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  697. var err error
  698. inReplyTo, part, err = getMessagePart(c, inReplyToPath.Mailbox, inReplyToPath.Uid, partPath)
  699. return err
  700. })
  701. if err != nil {
  702. return err
  703. }
  704. mimeType, _, err := part.Header.ContentType()
  705. if err != nil {
  706. return fmt.Errorf("failed to parse part Content-Type: %v", err)
  707. }
  708. if mimeType == "text/plain" {
  709. msg.Text, err = quote(part.Body)
  710. if err != nil {
  711. return err
  712. }
  713. } else if mimeType == "text/html" {
  714. text, err := html2text.FromReader(part.Body, html2text.Options{})
  715. if err != nil {
  716. return err
  717. }
  718. msg.Text, err = quote(strings.NewReader(text))
  719. if err != nil {
  720. return nil
  721. }
  722. } else {
  723. err := fmt.Errorf("cannot forward %q part", mimeType)
  724. return echo.NewHTTPError(http.StatusBadRequest, err)
  725. }
  726. var hdr mail.Header
  727. hdr.GenerateMessageID()
  728. mid, _ := hdr.MessageID()
  729. msg.MessageID = "<" + mid + ">"
  730. msg.InReplyTo = inReplyTo.Envelope.MessageId
  731. // TODO: populate From from known user addresses and inReplyTo.Envelope.To
  732. replyTo := inReplyTo.Envelope.ReplyTo
  733. if len(replyTo) == 0 {
  734. replyTo = inReplyTo.Envelope.From
  735. }
  736. msg.To = unwrapIMAPAddressList(replyTo)
  737. msg.Subject = inReplyTo.Envelope.Subject
  738. if !strings.HasPrefix(strings.ToLower(msg.Subject), "re:") {
  739. msg.Subject = "Re: " + msg.Subject
  740. }
  741. }
  742. return handleCompose(ctx, &msg, &composeOptions{InReplyTo: &inReplyToPath})
  743. }
  744. func handleForward(ctx *alps.Context) error {
  745. var sourcePath messagePath
  746. var err error
  747. sourcePath.Mailbox, sourcePath.Uid, err = parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
  748. if err != nil {
  749. return echo.NewHTTPError(http.StatusBadRequest, err)
  750. }
  751. var msg OutgoingMessage
  752. if ctx.Request().Method == http.MethodGet {
  753. // Populate fields from original message
  754. partPath, err := parsePartPath(ctx.QueryParam("part"))
  755. if err != nil {
  756. return echo.NewHTTPError(http.StatusBadRequest, err)
  757. }
  758. var source *IMAPMessage
  759. var part *message.Entity
  760. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  761. var err error
  762. source, part, err = getMessagePart(c, sourcePath.Mailbox, sourcePath.Uid, partPath)
  763. return err
  764. })
  765. if err != nil {
  766. return err
  767. }
  768. mimeType, _, err := part.Header.ContentType()
  769. if err != nil {
  770. return fmt.Errorf("failed to parse part Content-Type: %v", err)
  771. }
  772. if mimeType == "text/plain" {
  773. msg.Text, err = quote(part.Body)
  774. if err != nil {
  775. return err
  776. }
  777. } else if mimeType == "text/html" {
  778. msg.Text, err = html2text.FromReader(part.Body, html2text.Options{})
  779. if err != nil {
  780. return err
  781. }
  782. } else {
  783. err := fmt.Errorf("cannot forward %q part", mimeType)
  784. return echo.NewHTTPError(http.StatusBadRequest, err)
  785. }
  786. var hdr mail.Header
  787. hdr.GenerateMessageID()
  788. mid, _ := hdr.MessageID()
  789. msg.MessageID = "<" + mid + ">"
  790. msg.Subject = source.Envelope.Subject
  791. if !strings.HasPrefix(strings.ToLower(msg.Subject), "fwd:") &&
  792. !strings.HasPrefix(strings.ToLower(msg.Subject), "fw:") {
  793. msg.Subject = "Fwd: " + msg.Subject
  794. }
  795. msg.InReplyTo = source.Envelope.InReplyTo
  796. attachments := source.Attachments()
  797. for i := range attachments {
  798. // No need to populate attachment body here, we just need the
  799. // metadata
  800. msg.Attachments = append(msg.Attachments, &imapAttachment{
  801. Mailbox: sourcePath.Mailbox,
  802. Uid: sourcePath.Uid,
  803. Node: &attachments[i],
  804. })
  805. }
  806. }
  807. return handleCompose(ctx, &msg, &composeOptions{Forward: &sourcePath})
  808. }
  809. func handleEdit(ctx *alps.Context) error {
  810. var sourcePath messagePath
  811. var err error
  812. sourcePath.Mailbox, sourcePath.Uid, err = parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
  813. if err != nil {
  814. return echo.NewHTTPError(http.StatusBadRequest, err)
  815. }
  816. // TODO: somehow get the path to the In-Reply-To message (with a search?)
  817. var msg OutgoingMessage
  818. if ctx.Request().Method == http.MethodGet {
  819. // Populate fields from source message
  820. partPath, err := parsePartPath(ctx.QueryParam("part"))
  821. if err != nil {
  822. return echo.NewHTTPError(http.StatusBadRequest, err)
  823. }
  824. var source *IMAPMessage
  825. var part *message.Entity
  826. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  827. var err error
  828. source, part, err = getMessagePart(c, sourcePath.Mailbox, sourcePath.Uid, partPath)
  829. return err
  830. })
  831. if err != nil {
  832. return err
  833. }
  834. mimeType, _, err := part.Header.ContentType()
  835. if err != nil {
  836. return fmt.Errorf("failed to parse part Content-Type: %v", err)
  837. }
  838. if !strings.EqualFold(mimeType, "text/plain") {
  839. err := fmt.Errorf("cannot edit %q part", mimeType)
  840. return echo.NewHTTPError(http.StatusBadRequest, err)
  841. }
  842. b, err := ioutil.ReadAll(part.Body)
  843. if err != nil {
  844. return fmt.Errorf("failed to read part body: %v", err)
  845. }
  846. msg.Text = string(b)
  847. if len(source.Envelope.From) > 0 {
  848. msg.From = source.Envelope.From[0].Address()
  849. }
  850. msg.To = unwrapIMAPAddressList(source.Envelope.To)
  851. msg.Subject = source.Envelope.Subject
  852. msg.InReplyTo = source.Envelope.InReplyTo
  853. msg.MessageID = source.Envelope.MessageId
  854. attachments := source.Attachments()
  855. for i := range attachments {
  856. // No need to populate attachment body here, we just need the
  857. // metadata
  858. msg.Attachments = append(msg.Attachments, &imapAttachment{
  859. Mailbox: sourcePath.Mailbox,
  860. Uid: sourcePath.Uid,
  861. Node: &attachments[i],
  862. })
  863. }
  864. }
  865. return handleCompose(ctx, &msg, &composeOptions{Draft: &sourcePath})
  866. }
  867. func formOrQueryParam(ctx *alps.Context, k string) string {
  868. if v := ctx.FormValue(k); v != "" {
  869. return v
  870. }
  871. return ctx.QueryParam(k)
  872. }
  873. func handleMove(ctx *alps.Context) error {
  874. mboxName, err := url.PathUnescape(ctx.Param("mbox"))
  875. if err != nil {
  876. return echo.NewHTTPError(http.StatusBadRequest, err)
  877. }
  878. formParams, err := ctx.FormParams()
  879. if err != nil {
  880. return echo.NewHTTPError(http.StatusBadRequest, err)
  881. }
  882. uids, err := parseUidList(formParams["uids"])
  883. if err != nil {
  884. return echo.NewHTTPError(http.StatusBadRequest, err)
  885. }
  886. to := formOrQueryParam(ctx, "to")
  887. if to == "" {
  888. return echo.NewHTTPError(http.StatusBadRequest, "missing 'to' form parameter")
  889. }
  890. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  891. mc := imapmove.NewClient(c)
  892. if err := ensureMailboxSelected(c, mboxName); err != nil {
  893. return err
  894. }
  895. var seqSet imap.SeqSet
  896. seqSet.AddNum(uids...)
  897. if err := mc.UidMoveWithFallback(&seqSet, to); err != nil {
  898. return fmt.Errorf("failed to move message: %v", err)
  899. }
  900. // TODO: get the UID of the message in the destination mailbox with UIDPLUS
  901. return nil
  902. })
  903. if err != nil {
  904. return err
  905. }
  906. ctx.Session.PutNotice("Message(s) moved.")
  907. if path := formOrQueryParam(ctx, "next"); path != "" {
  908. return ctx.Redirect(http.StatusFound, path)
  909. }
  910. return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", url.PathEscape(mboxName)))
  911. }
  912. func handleDelete(ctx *alps.Context) error {
  913. mboxName, err := url.PathUnescape(ctx.Param("mbox"))
  914. if err != nil {
  915. return echo.NewHTTPError(http.StatusBadRequest, err)
  916. }
  917. formParams, err := ctx.FormParams()
  918. if err != nil {
  919. return echo.NewHTTPError(http.StatusBadRequest, err)
  920. }
  921. uids, err := parseUidList(formParams["uids"])
  922. if err != nil {
  923. return echo.NewHTTPError(http.StatusBadRequest, err)
  924. }
  925. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  926. if err := ensureMailboxSelected(c, mboxName); err != nil {
  927. return err
  928. }
  929. var seqSet imap.SeqSet
  930. seqSet.AddNum(uids...)
  931. item := imap.FormatFlagsOp(imap.AddFlags, true)
  932. flags := []interface{}{imap.DeletedFlag}
  933. if err := c.UidStore(&seqSet, item, flags, nil); err != nil {
  934. return fmt.Errorf("failed to add deleted flag: %v", err)
  935. }
  936. if err := c.Expunge(nil); err != nil {
  937. return fmt.Errorf("failed to expunge mailbox: %v", err)
  938. }
  939. // Deleting a message invalidates our cached message count
  940. // TODO: listen to async updates instead
  941. if _, err := c.Select(mboxName, false); err != nil {
  942. return fmt.Errorf("failed to select mailbox: %v", err)
  943. }
  944. return nil
  945. })
  946. if err != nil {
  947. return err
  948. }
  949. ctx.Session.PutNotice("Message(s) deleted.")
  950. if path := formOrQueryParam(ctx, "next"); path != "" {
  951. return ctx.Redirect(http.StatusFound, path)
  952. }
  953. return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", url.PathEscape(mboxName)))
  954. }
  955. func handleSetFlags(ctx *alps.Context) error {
  956. mboxName, err := url.PathUnescape(ctx.Param("mbox"))
  957. if err != nil {
  958. return echo.NewHTTPError(http.StatusBadRequest, err)
  959. }
  960. formParams, err := ctx.FormParams()
  961. if err != nil {
  962. return echo.NewHTTPError(http.StatusBadRequest, err)
  963. }
  964. uids, err := parseUidList(formParams["uids"])
  965. if err != nil {
  966. return echo.NewHTTPError(http.StatusBadRequest, err)
  967. }
  968. flags, ok := formParams["flags"]
  969. if !ok {
  970. flagsStr := ctx.QueryParam("to")
  971. if flagsStr == "" {
  972. return echo.NewHTTPError(http.StatusBadRequest, "missing 'flags' form parameter")
  973. }
  974. flags = strings.Fields(flagsStr)
  975. }
  976. actionStr := ctx.FormValue("action")
  977. if actionStr == "" {
  978. actionStr = ctx.QueryParam("action")
  979. }
  980. var op imap.FlagsOp
  981. switch actionStr {
  982. case "", "set":
  983. op = imap.SetFlags
  984. case "add":
  985. op = imap.AddFlags
  986. case "remove":
  987. op = imap.RemoveFlags
  988. default:
  989. return echo.NewHTTPError(http.StatusBadRequest, "invalid 'action' value")
  990. }
  991. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  992. if err := ensureMailboxSelected(c, mboxName); err != nil {
  993. return err
  994. }
  995. var seqSet imap.SeqSet
  996. seqSet.AddNum(uids...)
  997. storeItems := make([]interface{}, len(flags))
  998. for i, f := range flags {
  999. storeItems[i] = f
  1000. }
  1001. item := imap.FormatFlagsOp(op, true)
  1002. if err := c.UidStore(&seqSet, item, storeItems, nil); err != nil {
  1003. return fmt.Errorf("failed to add deleted flag: %v", err)
  1004. }
  1005. return nil
  1006. })
  1007. if err != nil {
  1008. return err
  1009. }
  1010. if path := formOrQueryParam(ctx, "next"); path != "" {
  1011. return ctx.Redirect(http.StatusFound, path)
  1012. }
  1013. if len(uids) != 1 || (op == imap.RemoveFlags && len(flags) == 1 && flags[0] == imap.SeenFlag) {
  1014. // Redirecting to the message view would mark the message as read again
  1015. return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", url.PathEscape(mboxName)))
  1016. }
  1017. return ctx.Redirect(http.StatusFound, fmt.Sprintf("/message/%v/%v", url.PathEscape(mboxName), uids[0]))
  1018. }
  1019. const settingsKey = "base.settings"
  1020. const maxMessagesPerPage = 100
  1021. type Settings struct {
  1022. MessagesPerPage int
  1023. Signature string
  1024. From string
  1025. Subscriptions []string
  1026. }
  1027. func loadSettings(s alps.Store) (*Settings, error) {
  1028. settings := &Settings{
  1029. MessagesPerPage: 50,
  1030. }
  1031. if err := s.Get(settingsKey, settings); err != nil && err != alps.ErrNoStoreEntry {
  1032. return nil, err
  1033. }
  1034. if err := settings.check(); err != nil {
  1035. return nil, err
  1036. }
  1037. return settings, nil
  1038. }
  1039. func (s *Settings) check() error {
  1040. if s.MessagesPerPage <= 0 || s.MessagesPerPage > maxMessagesPerPage {
  1041. return fmt.Errorf("messages per page out of bounds: %v", s.MessagesPerPage)
  1042. }
  1043. if len(s.Signature) > 2048 {
  1044. return fmt.Errorf("Signature must be 2048 characters or fewer")
  1045. }
  1046. if len(s.From) > 512 {
  1047. return fmt.Errorf("Full name must be 512 characters or fewer")
  1048. }
  1049. return nil
  1050. }
  1051. type SettingsRenderData struct {
  1052. alps.BaseRenderData
  1053. Mailboxes []MailboxInfo
  1054. Settings *Settings
  1055. Subscriptions Subscriptions
  1056. }
  1057. type Subscriptions []string
  1058. func (s Subscriptions) Has(sub string) bool {
  1059. for _, cand := range s {
  1060. if cand == sub {
  1061. return true
  1062. }
  1063. }
  1064. return false
  1065. }
  1066. func handleSettings(ctx *alps.Context) error {
  1067. settings, err := loadSettings(ctx.Session.Store())
  1068. if err != nil {
  1069. return fmt.Errorf("failed to load settings: %v", err)
  1070. }
  1071. var mailboxes []MailboxInfo
  1072. err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
  1073. mailboxes, err = listMailboxes(c)
  1074. return err
  1075. })
  1076. if err != nil {
  1077. return err
  1078. }
  1079. if ctx.Request().Method == http.MethodPost {
  1080. settings.MessagesPerPage, err = strconv.Atoi(ctx.FormValue("messages_per_page"))
  1081. if err != nil {
  1082. return echo.NewHTTPError(http.StatusBadRequest, "invalid messages per page: %v", err)
  1083. }
  1084. settings.Signature = ctx.FormValue("signature")
  1085. settings.From = ctx.FormValue("from")
  1086. params, err := ctx.FormParams()
  1087. if err != nil {
  1088. return err
  1089. }
  1090. settings.Subscriptions = params["subscriptions"]
  1091. if err := settings.check(); err != nil {
  1092. return echo.NewHTTPError(http.StatusBadRequest, err)
  1093. }
  1094. if err := ctx.Session.Store().Put(settingsKey, settings); err != nil {
  1095. return fmt.Errorf("failed to save settings: %v", err)
  1096. }
  1097. return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
  1098. }
  1099. return ctx.Render(http.StatusOK, "settings.html", &SettingsRenderData{
  1100. BaseRenderData: *alps.NewBaseRenderData(ctx),
  1101. Settings: settings,
  1102. Mailboxes: mailboxes,
  1103. Subscriptions: Subscriptions(settings.Subscriptions),
  1104. })
  1105. }