Add basic message view
This commit is contained in:
parent
fce17c9733
commit
25c63d0530
5 changed files with 211 additions and 9 deletions
1
go.mod
1
go.mod
|
@ -4,5 +4,6 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/emersion/go-imap v1.0.1
|
github.com/emersion/go-imap v1.0.1
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca
|
||||||
github.com/labstack/echo/v4 v4.1.11
|
github.com/labstack/echo/v4 v4.1.11
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -3,9 +3,11 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/emersion/go-imap v1.0.1 h1:J3duplefIrglQtE63hCGYdGLgMjYWqHvkUUEbimbXY8=
|
github.com/emersion/go-imap v1.0.1 h1:J3duplefIrglQtE63hCGYdGLgMjYWqHvkUUEbimbXY8=
|
||||||
github.com/emersion/go-imap v1.0.1/go.mod h1:MEiDDwwQFcZ+L45Pa68jNGv0qU9kbW+SJzwDpvSfX1s=
|
github.com/emersion/go-imap v1.0.1/go.mod h1:MEiDDwwQFcZ+L45Pa68jNGv0qU9kbW+SJzwDpvSfX1s=
|
||||||
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca h1:OYhqtJI4eOLvGtRIsUfP87VMJ1J/o6ks1tah9DlYkn4=
|
||||||
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
|
github.com/emersion/go-message v0.10.4-0.20190609165112-592ace5bc1ca/go.mod h1:3h+HsGTCFHmk4ngJ2IV/YPhdlaOcR6hcgqM3yca9v7c=
|
||||||
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317 h1:tYZxAY8nu3JJQKios9f27Sbvbkfm4XHXT476gVtszu0=
|
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317 h1:tYZxAY8nu3JJQKios9f27Sbvbkfm4XHXT476gVtszu0=
|
||||||
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
github.com/emersion/go-sasl v0.0.0-20190520160400-47d427600317/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||||
github.com/labstack/echo/v4 v4.1.11 h1:z0BZoArY4FqdpUEl+wlHp4hnr/oSR6MTmQmv8OHSoww=
|
github.com/labstack/echo/v4 v4.1.11 h1:z0BZoArY4FqdpUEl+wlHp4hnr/oSR6MTmQmv8OHSoww=
|
||||||
|
|
155
imap.go
155
imap.go
|
@ -1,9 +1,16 @@
|
||||||
package koushin
|
package koushin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
imapclient "github.com/emersion/go-imap/client"
|
imapclient "github.com/emersion/go-imap/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,17 +60,91 @@ func listMailboxes(conn *imapclient.Client) ([]*imap.MailboxInfo, error) {
|
||||||
return mailboxes, nil
|
return mailboxes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listMessages(conn *imapclient.Client, mboxName string) ([]*imap.Message, error) {
|
func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error {
|
||||||
mbox := conn.Mailbox()
|
mbox := conn.Mailbox()
|
||||||
if mbox == nil || mbox.Name != mboxName {
|
if mbox == nil || mbox.Name != mboxName {
|
||||||
var err error
|
if _, err := conn.Select(mboxName, false); err != nil {
|
||||||
mbox, err = conn.Select(mboxName, false)
|
return err
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type imapMessage struct {
|
||||||
|
*imap.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func textPartPath(bs *imap.BodyStructure) ([]int, bool) {
|
||||||
|
if bs.Disposition != "" && !strings.EqualFold(bs.Disposition, "inline") {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(bs.MIMEType, "text") {
|
||||||
|
return []int{1}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.EqualFold(bs.MIMEType, "multipart") {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
textPartNum := -1
|
||||||
|
for i, part := range bs.Parts {
|
||||||
|
num := i + 1
|
||||||
|
|
||||||
|
if strings.EqualFold(part.MIMEType, "multipart") {
|
||||||
|
if subpath, ok := textPartPath(part); ok {
|
||||||
|
return append([]int{num}, subpath...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(part.MIMEType, "text") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var pick bool
|
||||||
|
switch strings.ToLower(part.MIMESubType) {
|
||||||
|
case "plain":
|
||||||
|
pick = true
|
||||||
|
case "html":
|
||||||
|
pick = textPartNum < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if pick {
|
||||||
|
textPartNum = num
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if textPartNum > 0 {
|
||||||
|
return []int{textPartNum}, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *imapMessage) TextPartName() string {
|
||||||
|
if msg.BodyStructure == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
path, ok := textPartPath(msg.BodyStructure)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
l := make([]string, len(path))
|
||||||
|
for i, partNum := range path {
|
||||||
|
l[i] = strconv.Itoa(partNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(l, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listMessages(conn *imapclient.Client, mboxName string) ([]imapMessage, error) {
|
||||||
|
if err := ensureMailboxSelected(conn, mboxName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
n := uint32(10)
|
n := uint32(10)
|
||||||
|
|
||||||
|
mbox := conn.Mailbox()
|
||||||
from := uint32(1)
|
from := uint32(1)
|
||||||
to := mbox.Messages
|
to := mbox.Messages
|
||||||
if mbox.Messages > n {
|
if mbox.Messages > n {
|
||||||
|
@ -72,15 +153,17 @@ func listMessages(conn *imapclient.Client, mboxName string) ([]*imap.Message, er
|
||||||
seqSet := new(imap.SeqSet)
|
seqSet := new(imap.SeqSet)
|
||||||
seqSet.AddRange(from, to)
|
seqSet.AddRange(from, to)
|
||||||
|
|
||||||
|
fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
|
||||||
|
|
||||||
ch := make(chan *imap.Message, 10)
|
ch := make(chan *imap.Message, 10)
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
done <- conn.Fetch(seqSet, []imap.FetchItem{imap.FetchEnvelope}, ch)
|
done <- conn.Fetch(seqSet, fetch, ch)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
msgs := make([]*imap.Message, 0, n)
|
msgs := make([]imapMessage, 0, n)
|
||||||
for msg := range ch {
|
for msg := range ch {
|
||||||
msgs = append(msgs, msg)
|
msgs = append(msgs, imapMessage{msg})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := <-done; err != nil {
|
if err := <-done; err != nil {
|
||||||
|
@ -95,3 +178,59 @@ func listMessages(conn *imapclient.Client, mboxName string) ([]*imap.Message, er
|
||||||
|
|
||||||
return msgs, nil
|
return msgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ = message.Read
|
||||||
|
|
||||||
|
func getMessage(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*imap.Message, string, error) {
|
||||||
|
if err := ensureMailboxSelected(conn, mboxName); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
seqSet := new(imap.SeqSet)
|
||||||
|
seqSet.AddNum(uid)
|
||||||
|
|
||||||
|
var textHeaderSection imap.BodySectionName
|
||||||
|
textHeaderSection.Peek = true
|
||||||
|
textHeaderSection.Specifier = imap.HeaderSpecifier
|
||||||
|
textHeaderSection.Path = partPath
|
||||||
|
|
||||||
|
var textBodySection imap.BodySectionName
|
||||||
|
textBodySection.Peek = true
|
||||||
|
textBodySection.Path = partPath
|
||||||
|
|
||||||
|
fetch := []imap.FetchItem{
|
||||||
|
imap.FetchEnvelope,
|
||||||
|
imap.FetchUid,
|
||||||
|
imap.FetchBodyStructure,
|
||||||
|
textHeaderSection.FetchItem(),
|
||||||
|
textBodySection.FetchItem(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan *imap.Message, 1)
|
||||||
|
if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := <-ch
|
||||||
|
if msg == nil {
|
||||||
|
return nil, "", fmt.Errorf("server didn't return message")
|
||||||
|
}
|
||||||
|
|
||||||
|
headerReader := bufio.NewReader(msg.GetBody(&textHeaderSection))
|
||||||
|
h, err := textproto.ReadHeader(headerReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := message.New(message.Header{h}, msg.GetBody(&textBodySection))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(text.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg, string(b), nil
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
<h1>koushin</h1>
|
<h1>koushin</h1>
|
||||||
|
|
||||||
|
<h2>{{.Mailbox.Name}}</h2>
|
||||||
|
|
||||||
<p>Mailboxes:</p>
|
<p>Mailboxes:</p>
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Mailboxes}}
|
{{range .Mailboxes}}
|
||||||
|
@ -12,7 +14,9 @@
|
||||||
<p>Messages:</p>
|
<p>Messages:</p>
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Messages}}
|
{{range .Messages}}
|
||||||
<li>{{.Envelope.Subject}}</li>
|
<li><a href="/message/{{$.Mailbox.Name}}/{{.Uid}}?part={{.TextPartName}}">
|
||||||
|
{{.Envelope.Subject}}
|
||||||
|
</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
56
server.go
56
server.go
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
@ -94,6 +96,34 @@ func handleLogin(ectx echo.Context) error {
|
||||||
return ctx.Render(http.StatusOK, "login.html", nil)
|
return ctx.Render(http.StatusOK, "login.html", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseUid(s string) (uint32, error) {
|
||||||
|
uid, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if uid == 0 {
|
||||||
|
return 0, fmt.Errorf("UID must be non-zero")
|
||||||
|
}
|
||||||
|
return uint32(uid), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePartPath(s string) ([]int, error) {
|
||||||
|
l := strings.Split(s, ".")
|
||||||
|
path := make([]int, len(l))
|
||||||
|
for i, s := range l {
|
||||||
|
var err error
|
||||||
|
path[i], err = strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if path[i] <= 0 {
|
||||||
|
return nil, fmt.Errorf("part num must be strictly positive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
func New(imapURL string) *echo.Echo {
|
func New(imapURL string) *echo.Echo {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
|
@ -149,11 +179,37 @@ func New(imapURL string) *echo.Echo {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.Render(http.StatusOK, "mailbox.html", map[string]interface{}{
|
return ctx.Render(http.StatusOK, "mailbox.html", map[string]interface{}{
|
||||||
|
"Mailbox": ctx.conn.Mailbox(),
|
||||||
"Mailboxes": mailboxes,
|
"Mailboxes": mailboxes,
|
||||||
"Messages": msgs,
|
"Messages": msgs,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
e.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
|
||||||
|
ctx := ectx.(*context)
|
||||||
|
|
||||||
|
uid, err := parseUid(ctx.Param("uid"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
// TODO: handle messages without a text part
|
||||||
|
part, err := parsePartPath(ctx.QueryParam("part"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, body, err := getMessage(ctx.conn, ctx.Param("mbox"), uid, part)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Render(http.StatusOK, "message.html", map[string]interface{}{
|
||||||
|
"Mailbox": ctx.conn.Mailbox(),
|
||||||
|
"Message": msg,
|
||||||
|
"Body": body,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
e.GET("/login", handleLogin)
|
e.GET("/login", handleLogin)
|
||||||
e.POST("/login", handleLogin)
|
e.POST("/login", handleLogin)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue