16 Commits

Author SHA1 Message Date
  j3s b5fcf10c44 Repoint repository references to new location 3 years ago
  Drew DeVault 51d762ac5f Implement mailbox subscriptions 3 years ago
  Drew DeVault 8cc742f45d Fix issues with to/from headers 3 years ago
  Drew DeVault 61cdb93e48 s/email/text/ in To & From fields 3 years ago
  Drew DeVault 797b426ec6 Add "Full name" option to settings 3 years ago
  Drew DeVault 51498a2dc3 JS enhancements to encourage bottom-posting 3 years ago
  Drew DeVault a1e8bcc561 Implement message signature setting 3 years ago
  Drew DeVault a5d2af2c4e Remove "this email was deleted by another client" 3 years ago
  Drew DeVault cb37df882e Add notices on action completion 3 years ago
  Drew DeVault 405c18d213 Convert HTML to plaintext for forwarding & replies 3 years ago
  Drew DeVault 2fb78cad97 Fix name of HTML tab in HTML-only emails 3 years ago
  Drew DeVault 2430473dbc Comment out subscription buttons 3 years ago
  Drew DeVault 8771ddeb2d Add favicons for alps theme 3 years ago
  Drew DeVault 1992880454 Add theme-specific error page 3 years ago
  Drew DeVault 5087e4b327 Improve "attachments exceed max size" error message 3 years ago
  Drew DeVault 297afc5ce6 Limit total size of unsent attachments 3 years ago
41 changed files with 420 additions and 148 deletions
Split View
  1. +1
    -1
      .build.yml
  2. +6
    -6
      README.md
  3. +7
    -10
      cmd/alps/main.go
  4. +2
    -2
      docs/example-go-plugin/plugin.go
  5. +5
    -2
      go.mod
  6. +16
    -0
      go.sum
  7. +1
    -1
      plugins/base/plugin.go
  8. +161
    -37
      plugins/base/routes.go
  9. +17
    -6
      plugins/base/smtp.go
  10. +1
    -1
      plugins/base/viewer.go
  11. +1
    -1
      plugins/caldav/caldav.go
  12. +1
    -1
      plugins/caldav/plugin.go
  13. +1
    -1
      plugins/caldav/routes.go
  14. +1
    -1
      plugins/carddav/carddav.go
  15. +2
    -2
      plugins/carddav/plugin.go
  16. +1
    -1
      plugins/carddav/routes.go
  17. +1
    -1
      plugins/lua/lua.go
  18. +1
    -1
      plugins/lua/plugin.go
  19. +2
    -2
      plugins/viewhtml/plugin.go
  20. +1
    -1
      plugins/viewhtml/sanitize.go
  21. +2
    -2
      plugins/viewhtml/viewer.go
  22. +1
    -1
      plugins/viewtext/plugin.go
  23. +2
    -2
      plugins/viewtext/viewer.go
  24. +12
    -4
      renderer.go
  25. +22
    -6
      server.go
  26. +26
    -9
      session.go
  27. +8
    -0
      themes/alps/assets/compose.js
  28. BIN
      themes/alps/assets/favicon-128.png
  29. BIN
      themes/alps/assets/favicon-16x16.png
  30. BIN
      themes/alps/assets/favicon-196x196.png
  31. BIN
      themes/alps/assets/favicon-32x32.png
  32. BIN
      themes/alps/assets/favicon-96x96.png
  33. +29
    -4
      themes/alps/assets/style.css
  34. +2
    -4
      themes/alps/compose.html
  35. +14
    -0
      themes/alps/error.html
  36. +5
    -0
      themes/alps/head.html
  37. +1
    -11
      themes/alps/mailbox.html
  38. +7
    -1
      themes/alps/message.html
  39. +8
    -0
      themes/alps/nav.html
  40. +41
    -1
      themes/alps/settings.html
  41. +11
    -25
      themes/alps/util.html

+ 1
- 1
.build.yml View File

@@ -2,7 +2,7 @@ image: alpine/edge
packages:
- go
sources:
- https://git.sr.ht/~emersion/alps
- https://git.sr.ht/~migadu/alps
tasks:
- build: |
cd alps


+ 6
- 6
README.md View File

@@ -1,7 +1,7 @@
# [alps]

[![GoDoc](https://godoc.org/git.sr.ht/~emersion/alps?status.svg)](https://godoc.org/git.sr.ht/~emersion/alps)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/alps/commits.svg)](https://builds.sr.ht/~emersion/alps/commits?)
[![GoDoc](https://godoc.org/git.sr.ht/~migadu/alps?status.svg)](https://godoc.org/git.sr.ht/~migadu/alps)
[![builds.sr.ht status](https://builds.sr.ht/~migadu/alps/commits.svg)](https://builds.sr.ht/~migadu/alps/commits?)

A simple and extensible webmail.

@@ -29,8 +29,8 @@ Send patches on the [mailing list], report bugs on the [issue tracker].

MIT

[alps]: https://sr.ht/~emersion/alps
[alps]: https://sr.ht/~migadu/alps
[RFC 6186]: https://tools.ietf.org/html/rfc6186
[Go plugin helpers]: https://godoc.org/git.sr.ht/~emersion/alps#GoPlugin
[mailing list]: https://lists.sr.ht/~emersion/alps-dev
[issue tracker]: https://todo.sr.ht/~emersion/alps
[Go plugin helpers]: https://godoc.org/git.sr.ht/~migadu/alps#GoPlugin
[mailing list]: https://lists.sr.ht/~migadu/alps-dev
[issue tracker]: https://todo.sr.ht/~migadu/alps

+ 7
- 10
cmd/alps/main.go View File

@@ -9,18 +9,18 @@ import (
"syscall"
"time"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/fernet/fernet-go"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"

_ "git.sr.ht/~emersion/alps/plugins/base"
_ "git.sr.ht/~emersion/alps/plugins/caldav"
_ "git.sr.ht/~emersion/alps/plugins/carddav"
_ "git.sr.ht/~emersion/alps/plugins/lua"
_ "git.sr.ht/~emersion/alps/plugins/viewhtml"
_ "git.sr.ht/~emersion/alps/plugins/viewtext"
_ "git.sr.ht/~migadu/alps/plugins/base"
_ "git.sr.ht/~migadu/alps/plugins/caldav"
_ "git.sr.ht/~migadu/alps/plugins/carddav"
_ "git.sr.ht/~migadu/alps/plugins/lua"
_ "git.sr.ht/~migadu/alps/plugins/viewhtml"
_ "git.sr.ht/~migadu/alps/plugins/viewtext"
)

func main() {
@@ -70,9 +70,6 @@ func main() {
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339} method=${method}, uri=${uri}, status=${status}\n",
}))
}

if options.Debug {
e.Logger.SetLevel(log.DEBUG)
}



+ 2
- 2
docs/example-go-plugin/plugin.go View File

@@ -7,8 +7,8 @@ import (
"fmt"
"net/http"

"git.sr.ht/~emersion/alps"
alpsbase "git.sr.ht/~emersion/alps/plugins/base"
"git.sr.ht/~migadu/alps"
alpsbase "git.sr.ht/~migadu/alps/plugins/base"
)

func init() {


+ 5
- 2
go.mod View File

@@ -1,4 +1,4 @@
module git.sr.ht/~emersion/alps
module git.sr.ht/~migadu/alps

go 1.13

@@ -12,7 +12,7 @@ require (
github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62
github.com/emersion/go-message v0.11.2
github.com/emersion/go-message v0.13.1-0.20201112194930-f77964fe28bd
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.13.0
github.com/emersion/go-vcard v0.0.0-20200508080525-dd3110a24ec2
@@ -23,9 +23,12 @@ require (
github.com/labstack/echo/v4 v4.1.16
github.com/labstack/gommon v0.3.0
github.com/microcosm-cc/bluemonday v1.0.2
github.com/olekukonko/tablewriter v0.0.4 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7
layeh.com/gopher-luar v1.0.7
)

+ 16
- 0
go.sum View File

@@ -42,6 +42,8 @@ github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dS
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.11.2 h1:oxO9SQ+3wgBAQRdk07eqfkCJ26Tl8ZHF7CcpGVoE00o=
github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.13.1-0.20201112194930-f77964fe28bd h1:6CXxdoOzAyQForkd2U/JNceVyNpmg92alCU2R+4dwIY=
github.com/emersion/go-message v0.13.1-0.20201112194930-f77964fe28bd/go.mod h1:SXSs/8KamlsyxjpHL1Q3yf5Jrv7QG5icuvPK1SMcnzw=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
@@ -50,6 +52,8 @@ github.com/emersion/go-smtp v0.13.0 h1:aC3Kc21TdfvXnuJXCQXuhnDXUldhc12qME/S7Y3Y9
github.com/emersion/go-smtp v0.13.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
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-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7 h1:SE+tcd+0kn0cT4MqTo66gmkjqWHF1Z+Yha5/rhLs/H8=
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-vcard v0.0.0-20200508080525-dd3110a24ec2 h1:g1RgqggIPPkEBubnOxAbIglxjMsP0aHPO2ryigBHu2s=
@@ -96,6 +100,8 @@ github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/martinlindhe/base36 v1.1.0 h1:cIwvvwYse/0+1CkUPYH5ZvVIYG3JrILmQEIbLuar02Y=
github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -103,6 +109,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
@@ -112,6 +120,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -133,6 +143,8 @@ github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFB
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -186,6 +198,8 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e h1:0kyKOEC0chG7FKmnf/1uNwvDLc3NtNTRip2rXAN9nwI=
golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -205,5 +219,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:mub0MmFLOn8XLikZOAhgLD1kXJq8jgftSrrv7m00xFo=
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
layeh.com/gopher-luar v1.0.7 h1:53iv6CCkRs5wyofZ+qVXcyAYQOIG52s6pt4xkqZdq7k=
layeh.com/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=

+ 1
- 1
plugins/base/plugin.go View File

@@ -1,7 +1,7 @@
package alpsbase

import (
"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
)

func init() {


+ 161
- 37
plugins/base/routes.go View File

@@ -12,14 +12,15 @@ import (
"strconv"
"strings"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/emersion/go-imap"
imapmove "github.com/emersion/go-imap-move"
imapclient "github.com/emersion/go-imap/client"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-smtp"
"github.com/labstack/echo/v4"
"jaytaylor.com/html2text"
imapclient "github.com/emersion/go-imap/client"
imapmove "github.com/emersion/go-imap-move"
)

func registerRoutes(p *alps.GoPlugin) {
@@ -79,6 +80,7 @@ type IMAPBaseRenderData struct {
Mailboxes []MailboxInfo
Mailbox *MailboxStatus
Inbox *MailboxStatus
Subscriptions map[string]*MailboxStatus
}

type MailboxRenderData struct {
@@ -88,17 +90,22 @@ type MailboxRenderData struct {
Query string
}

type MailboxDetails struct {
Info *MailboxInfo
Status *MailboxStatus
}

// Organizes mailboxes into common/uncommon categories
type CategorizedMailboxes struct {
Common struct {
Inbox *MailboxInfo
Drafts *MailboxInfo
Sent *MailboxInfo
Junk *MailboxInfo
Trash *MailboxInfo
Archive *MailboxInfo
}
Additional []*MailboxInfo
Inbox MailboxDetails
Drafts MailboxDetails
Sent MailboxDetails
Junk MailboxDetails
Trash MailboxDetails
Archive MailboxDetails
}
Additional []MailboxDetails
}

func newIMAPBaseRenderData(ctx *alps.Context,
@@ -109,6 +116,12 @@ func newIMAPBaseRenderData(ctx *alps.Context,
return nil, echo.NewHTTPError(http.StatusBadRequest, err)
}

settings, err := loadSettings(ctx.Session.Store())
if err != nil {
return nil, fmt.Errorf("failed to load settings: %v", err)
}

subscriptions := make(map[string]*MailboxStatus)
var mailboxes []MailboxInfo
var active, inbox *MailboxStatus
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
@@ -116,11 +129,13 @@ func newIMAPBaseRenderData(ctx *alps.Context,
if mailboxes, err = listMailboxes(c); err != nil {
return err
}

if mboxName != "" {
if active, err = getMailboxStatus(c, mboxName); err != nil {
return err
return echo.NewHTTPError(http.StatusNotFound, err)
}
}

if mboxName == "INBOX" {
inbox = active
} else {
@@ -128,6 +143,14 @@ func newIMAPBaseRenderData(ctx *alps.Context,
return err
}
}

for _, sub := range settings.Subscriptions {
if status, err := getMailboxStatus(c, sub); err != nil {
return err
} else {
subscriptions[sub] = status
}
}
return nil
})
if err != nil {
@@ -135,7 +158,7 @@ func newIMAPBaseRenderData(ctx *alps.Context,
}

var categorized CategorizedMailboxes
mmap := map[string]**MailboxInfo{
mmap := map[string]*MailboxDetails{
"INBOX": &categorized.Common.Inbox,
"Drafts": &categorized.Common.Drafts,
"Sent": &categorized.Common.Sent,
@@ -156,11 +179,16 @@ func newIMAPBaseRenderData(ctx *alps.Context,
mailboxes[i].Total = int(inbox.Messages)
}

status, _ := subscriptions[mailboxes[i].Name]
if ptr, ok := mmap[mailboxes[i].Name]; ok {
*ptr = &mailboxes[i]
ptr.Info = &mailboxes[i]
ptr.Status = status
} else {
categorized.Additional = append(
categorized.Additional, &mailboxes[i])
categorized.Additional = append(categorized.Additional,
MailboxDetails{
Info: &mailboxes[i],
Status: status,
})
}
}

@@ -170,6 +198,7 @@ func newIMAPBaseRenderData(ctx *alps.Context,
Mailboxes: mailboxes,
Inbox: inbox,
Mailbox: active,
Subscriptions: subscriptions,
}, nil
}

@@ -305,6 +334,7 @@ func handleDeleteMailbox(ctx *alps.Context) error {
ctx.Session.DoIMAP(func(c *imapclient.Client) error {
return c.Delete(mbox.Name)
})
ctx.Session.PutNotice("Mailbox deleted.")
return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
}

@@ -518,6 +548,7 @@ func submitCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
return fmt.Errorf("failed to save message to Sent mailbox: %v", err)
}

ctx.Session.PutNotice("Message sent.")
return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
}

@@ -528,7 +559,19 @@ func handleCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
}

if msg.From == "" && strings.ContainsRune(ctx.Session.Username(), '@') {
msg.From = ctx.Session.Username()
settings, err := loadSettings(ctx.Session.Store())
if err != nil {
return err
}
if settings.From != "" {
addr := mail.Address{
Name: settings.From,
Address: ctx.Session.Username(),
}
msg.From = addr.String()
} else {
msg.From = ctx.Session.Username()
}
}

if ctx.Request().Method == http.MethodPost {
@@ -650,6 +693,7 @@ func handleCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
if err != nil {
return fmt.Errorf("failed to save message to Draft mailbox: %v", err)
}
ctx.Session.PutNotice("Message saved as draft.")
return ctx.Redirect(http.StatusFound, fmt.Sprintf(
"/message/%s/%d/edit?part=1", drafts.Name, uid))
} else {
@@ -664,14 +708,26 @@ func handleCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti
}

func handleComposeNew(ctx *alps.Context) error {
text := ctx.QueryParam("body")
settings, err := loadSettings(ctx.Session.Store())
if err != nil {
return nil
}
if text == "" && settings.Signature != "" {
text = "\n\n\n-- \n" + settings.Signature
}

// These are common mailto URL query parameters
// TODO: cc, bcc
var hdr mail.Header
hdr.GenerateMessageID()
mid, _ := hdr.MessageID()
return handleCompose(ctx, &OutgoingMessage{
To: strings.Split(ctx.QueryParam("to"), ","),
Subject: ctx.QueryParam("subject"),
Text: ctx.QueryParam("body"),
MessageID: mail.GenerateMessageID(),
MessageID: "<" + mid + ">",
InReplyTo: ctx.QueryParam("in-reply-to"),
Text: text,
}, &composeOptions{})
}

@@ -692,7 +748,13 @@ func handleComposeAttachment(ctx *alps.Context) error {
var uuids []string
for _, fh := range form.File["attachments"] {
uuid, err := ctx.Session.PutAttachment(fh, form)
if err != nil {
if err == alps.ErrAttachmentCacheSize {
form.RemoveAll()
return ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "Your attachments exceed the maximum file size. Remove some and try again.",
})
} else if err != nil {
form.RemoveAll()
ctx.Logger().Printf("PutAttachment: %v\n", err)
return ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "failed to store attachment",
@@ -753,18 +815,29 @@ func handleReply(ctx *alps.Context) error {
return fmt.Errorf("failed to parse part Content-Type: %v", err)
}

if !strings.EqualFold(mimeType, "text/plain") {
err := fmt.Errorf("cannot reply to %q part", mimeType)
if mimeType == "text/plain" {
msg.Text, err = quote(part.Body)
if err != nil {
return err
}
} else if mimeType == "text/html" {
text, err := html2text.FromReader(part.Body, html2text.Options{})
if err != nil {
return err
}
msg.Text, err = quote(strings.NewReader(text))
if err != nil {
return nil
}
} else {
err := fmt.Errorf("cannot forward %q part", mimeType)
return echo.NewHTTPError(http.StatusBadRequest, err)
}

// TODO: strip HTML tags if text/html
msg.Text, err = quote(part.Body)
if err != nil {
return err
}

msg.MessageID = mail.GenerateMessageID()
var hdr mail.Header
hdr.GenerateMessageID()
mid, _ := hdr.MessageID()
msg.MessageID = "<" + mid + ">"
msg.InReplyTo = inReplyTo.Envelope.MessageId
// TODO: populate From from known user addresses and inReplyTo.Envelope.To
replyTo := inReplyTo.Envelope.ReplyTo
@@ -813,17 +886,25 @@ func handleForward(ctx *alps.Context) error {
return fmt.Errorf("failed to parse part Content-Type: %v", err)
}

if !strings.EqualFold(mimeType, "text/plain") {
if mimeType == "text/plain" {
msg.Text, err = quote(part.Body)
if err != nil {
return err
}
} else if mimeType == "text/html" {
msg.Text, err = html2text.FromReader(part.Body, html2text.Options{})
if err != nil {
return err
}
} else {
err := fmt.Errorf("cannot forward %q part", mimeType)
return echo.NewHTTPError(http.StatusBadRequest, err)
}

msg.Text, err = quote(part.Body)
if err != nil {
return err
}

msg.MessageID = mail.GenerateMessageID()
var hdr mail.Header
hdr.GenerateMessageID()
mid, _ := hdr.MessageID()
msg.MessageID = "<" + mid + ">"
msg.Subject = source.Envelope.Subject
if !strings.HasPrefix(strings.ToLower(msg.Subject), "fwd:") &&
!strings.HasPrefix(strings.ToLower(msg.Subject), "fw:") {
@@ -961,6 +1042,7 @@ func handleMove(ctx *alps.Context) error {
return err
}

ctx.Session.PutNotice("Message(s) moved.")
if path := formOrQueryParam(ctx, "next"); path != "" {
return ctx.Redirect(http.StatusFound, path)
}
@@ -1012,6 +1094,7 @@ func handleDelete(ctx *alps.Context) error {
return err
}

ctx.Session.PutNotice("Message(s) deleted.")
if path := formOrQueryParam(ctx, "next"); path != "" {
return ctx.Redirect(http.StatusFound, path)
}
@@ -1099,6 +1182,9 @@ const maxMessagesPerPage = 100

type Settings struct {
MessagesPerPage int
Signature string
From string
Subscriptions []string
}

func loadSettings(s alps.Store) (*Settings, error) {
@@ -1118,12 +1204,31 @@ func (s *Settings) check() error {
if s.MessagesPerPage <= 0 || s.MessagesPerPage > maxMessagesPerPage {
return fmt.Errorf("messages per page out of bounds: %v", s.MessagesPerPage)
}
if len(s.Signature) > 2048 {
return fmt.Errorf("Signature must be 2048 characters or fewer")
}
if len(s.From) > 512 {
return fmt.Errorf("Full name must be 512 characters or fewer")
}
return nil
}

type SettingsRenderData struct {
alps.BaseRenderData
Settings *Settings
Mailboxes []MailboxInfo
Settings *Settings
Subscriptions Subscriptions
}

type Subscriptions []string

func (s Subscriptions) Has(sub string) bool {
for _, cand := range s {
if cand == sub {
return true
}
}
return false
}

func handleSettings(ctx *alps.Context) error {
@@ -1132,11 +1237,28 @@ func handleSettings(ctx *alps.Context) error {
return fmt.Errorf("failed to load settings: %v", err)
}

var mailboxes []MailboxInfo
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
mailboxes, err = listMailboxes(c)
return err
})
if err != nil {
return err
}

if ctx.Request().Method == http.MethodPost {
settings.MessagesPerPage, err = strconv.Atoi(ctx.FormValue("messages_per_page"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid messages per page: %v", err)
}
settings.Signature = ctx.FormValue("signature")
settings.From = ctx.FormValue("from")

params, err := ctx.FormParams()
if err != nil {
return err
}
settings.Subscriptions = params["subscriptions"]

if err := settings.check(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
@@ -1151,5 +1273,7 @@ func handleSettings(ctx *alps.Context) error {
return ctx.Render(http.StatusOK, "settings.html", &SettingsRenderData{
BaseRenderData: *alps.NewBaseRenderData(ctx),
Settings: settings,
Mailboxes: mailboxes,
Subscriptions: Subscriptions(settings.Subscriptions),
})
}

+ 17
- 6
plugins/base/smtp.go View File

@@ -26,6 +26,7 @@ func quote(r io.Reader) (string, error) {
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("quote: failed to read original message: %s", err)
}
builder.WriteString("\n")
return builder.String(), nil
}

@@ -122,11 +123,19 @@ func writeAttachment(mw *mail.Writer, att Attachment) error {
}

func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
from := []*mail.Address{{"", msg.From}}
fromAddr, err := mail.ParseAddress(msg.From)
if err != nil {
return err
}
from := []*mail.Address{fromAddr}

to := make([]*mail.Address, len(msg.To))
for i, addr := range msg.To {
to[i] = &mail.Address{"", addr}
for i, rcpt := range msg.To {
addr, err := mail.ParseAddress(rcpt)
if err != nil {
return err
}
to[i] = addr
}

var h mail.Header
@@ -181,13 +190,15 @@ func (msg *OutgoingMessage) WriteTo(w io.Writer) error {
}

func sendMessage(c *smtp.Client, msg *OutgoingMessage) error {
if err := c.Mail(msg.From, nil); err != nil {
addr, _ := mail.ParseAddress(msg.From)
if err := c.Mail(addr.Address, nil); err != nil {
return fmt.Errorf("MAIL FROM failed: %v", err)
}

for _, to := range msg.To {
if err := c.Rcpt(to); err != nil {
return fmt.Errorf("RCPT TO failed: %v", err)
addr, _ := mail.ParseAddress(to)
if err := c.Rcpt(addr.Address); err != nil {
return fmt.Errorf("RCPT TO failed: %v (%s)", err, addr.Address)
}
}



+ 1
- 1
plugins/base/viewer.go View File

@@ -3,7 +3,7 @@ package alpsbase
import (
"fmt"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/emersion/go-message"
)



+ 1
- 1
plugins/caldav/caldav.go View File

@@ -5,7 +5,7 @@ import (
"net/http"
"net/url"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/emersion/go-webdav/caldav"
)



+ 1
- 1
plugins/caldav/plugin.go View File

@@ -5,7 +5,7 @@ import (
"net/http"
"net/url"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
)

const (


+ 1
- 1
plugins/caldav/routes.go View File

@@ -8,7 +8,7 @@ import (
"strings"
"time"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/emersion/go-ical"
"github.com/emersion/go-webdav/caldav"
"github.com/google/uuid"


+ 1
- 1
plugins/carddav/carddav.go View File

@@ -5,7 +5,7 @@ import (
"net/http"
"net/url"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/emersion/go-webdav/carddav"
)



+ 2
- 2
plugins/carddav/plugin.go View File

@@ -5,8 +5,8 @@ import (
"net/http"
"net/url"

"git.sr.ht/~emersion/alps"
alpsbase "git.sr.ht/~emersion/alps/plugins/base"
"git.sr.ht/~migadu/alps"
alpsbase "git.sr.ht/~migadu/alps/plugins/base"
"github.com/emersion/go-vcard"
"github.com/emersion/go-webdav/carddav"
)


+ 1
- 1
plugins/carddav/routes.go View File

@@ -7,7 +7,7 @@ import (
"path"
"strings"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/emersion/go-vcard"
"github.com/emersion/go-webdav/carddav"
"github.com/google/uuid"


+ 1
- 1
plugins/lua/lua.go View File

@@ -5,7 +5,7 @@ import (
"html/template"
"path/filepath"

"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
"github.com/labstack/echo/v4"
"github.com/yuin/gopher-lua"
"layeh.com/gopher-luar"


+ 1
- 1
plugins/lua/plugin.go View File

@@ -1,7 +1,7 @@
package alpslua

import (
"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
)

func init() {


+ 2
- 2
plugins/viewhtml/plugin.go View File

@@ -8,8 +8,8 @@ import (
"strconv"
"strings"

"git.sr.ht/~emersion/alps"
alpsbase "git.sr.ht/~emersion/alps/plugins/base"
"git.sr.ht/~migadu/alps"
alpsbase "git.sr.ht/~migadu/alps/plugins/base"
"github.com/labstack/echo/v4"
)



+ 1
- 1
plugins/viewhtml/sanitize.go View File

@@ -7,7 +7,7 @@ import (
"regexp"
"strings"

alpsbase "git.sr.ht/~emersion/alps/plugins/base"
alpsbase "git.sr.ht/~migadu/alps/plugins/base"
"github.com/aymerick/douceur/css"
cssparser "github.com/chris-ramon/douceur/parser"
"github.com/microcosm-cc/bluemonday"


+ 2
- 2
plugins/viewhtml/viewer.go View File

@@ -7,8 +7,8 @@ import (
"io/ioutil"
"strings"

"git.sr.ht/~emersion/alps"
alpsbase "git.sr.ht/~emersion/alps/plugins/base"
"git.sr.ht/~migadu/alps"
alpsbase "git.sr.ht/~migadu/alps/plugins/base"
"github.com/emersion/go-message"
)



+ 1
- 1
plugins/viewtext/plugin.go View File

@@ -1,7 +1,7 @@
package alpsviewtext

import (
"git.sr.ht/~emersion/alps"
"git.sr.ht/~migadu/alps"
)

func init() {


+ 2
- 2
plugins/viewtext/viewer.go View File

@@ -7,8 +7,8 @@ import (
"net/url"
"strings"

"git.sr.ht/~emersion/alps"
alpsbase "git.sr.ht/~emersion/alps/plugins/base"
"git.sr.ht/~migadu/alps"
alpsbase "git.sr.ht/~migadu/alps/plugins/base"
"github.com/emersion/go-message"
"gitlab.com/golang-commonmark/linkify"
)


+ 12
- 4
renderer.go View File

@@ -28,6 +28,8 @@ type GlobalRenderData struct {

HavePlugin func(name string) bool

Notice string

// additional plugin-specific data
Extra map[string]interface{}
}
@@ -70,14 +72,19 @@ type RenderData interface {
// BaseRenderData: *alps.NewBaseRenderData(ctx),
// // other fields...
// }
func NewBaseRenderData(ctx *Context) *BaseRenderData {
func NewBaseRenderData(ectx echo.Context) *BaseRenderData {
ctx, isactx := ectx.(*Context)

global := GlobalRenderData{
Extra: make(map[string]interface{}),
Path: strings.Split(ctx.Request().URL.Path, "/")[1:],
Path: strings.Split(ectx.Request().URL.Path, "/")[1:],
Title: "Webmail",
URL: ctx.Request().URL,
URL: ectx.Request().URL,

HavePlugin: func(name string) bool {
if !isactx {
return false
}
for _, plugin := range ctx.Server.plugins {
if plugin.Name() == name {
return true
@@ -87,9 +94,10 @@ func NewBaseRenderData(ctx *Context) *BaseRenderData {
},
}

if ctx.Session != nil {
if isactx && ctx.Session != nil {
global.LoggedIn = true
global.Username = ctx.Session.username
global.Notice = ctx.Session.PopNotice()
}

return &BaseRenderData{


+ 22
- 6
server.go View File

@@ -382,15 +382,31 @@ func New(e *echo.Echo, options *Options) (*Server, error) {
return nil, err
}

e.HTTPErrorHandler = func(err error, c echo.Context) {
e.HTTPErrorHandler = func(err error, ctx echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
} else {
c.Logger().Error(err)
}
// TODO: hide internal errors
c.String(code, err.Error())

type ErrorRenderData struct {
BaseRenderData
Code int
Err error
Status string
}
rdata := ErrorRenderData{
BaseRenderData: *NewBaseRenderData(ctx),
Err: err,
Code: code,
Status: http.StatusText(code),
}

if err := ctx.Render(code, "error.html", &rdata); err != nil {
ctx.Logger().Error(fmt.Errorf(
"Error occured rendering error page: %w. How meta.", err))
}

ctx.Logger().Error(err)
}

e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -426,7 +442,7 @@ func New(e *echo.Echo, options *Options) (*Server, error) {
}

ctx.Session, err = ctx.Server.Sessions.get(cookie.Value)
if err == errSessionExpired {
if err == ErrSessionExpired {
ctx.SetSession(nil)
return handleUnauthenticated(next, ctx)
} else if err != nil {


+ 26
- 9
session.go View File

@@ -20,6 +20,7 @@ import (

// TODO: make this configurable
const sessionDuration = 30 * time.Minute
const maxAttachmentSize = 32 << 20 // 32 MiB

func generateToken() (string, error) {
b := make([]byte, 32)
@@ -30,7 +31,10 @@ func generateToken() (string, error) {
return base64.URLEncoding.EncodeToString(b), nil
}

var errSessionExpired = errors.New("session expired")
var (
ErrSessionExpired = errors.New("session expired")
ErrAttachmentCacheSize = errors.New("Attachments on session exceed maximum file size")
)

// AuthError wraps an authentication error.
type AuthError struct {
@@ -53,6 +57,7 @@ type Session struct {
pings chan struct{}
timer *time.Timer
store Store
notice string

imapLocker sync.Mutex
imapConn *imapclient.Client // protected by locker, can be nil
@@ -145,15 +150,17 @@ func (s *Session) Close() {
// Puts an attachment and returns a generated UUID
func (s *Session) PutAttachment(in *multipart.FileHeader,
form *multipart.Form) (string, error) {
// TODO: Prevent users from uploading too many attachments, or too large
//
// Probably just set a cap on the maximum combined size of all files in the
// user's session
//
// TODO: Figure out what to do if the user abandons the compose window
// after adding some attachments
id := uuid.New()
s.attachmentsLocker.Lock()

var size int64
for _, a := range s.attachments {
size += a.File.Size
}
if size + in.Size > maxAttachmentSize {
return "", ErrAttachmentCacheSize
}

s.attachments[id.String()] = &Attachment{
File: in,
Form: form,
@@ -177,6 +184,16 @@ func (s *Session) PopAttachment(uuid string) *Attachment {
return a
}

func (s *Session) PutNotice(n string) {
s.notice = n
}

func (s *Session) PopNotice() string {
n := s.notice
s.notice = ""
return n
}

// Store returns a store suitable for storing persistent user data.
func (s *Session) Store() Store {
return s.store
@@ -241,7 +258,7 @@ func (sm *SessionManager) get(token string) (*Session, error) {

session, ok := sm.sessions[token]
if !ok {
return nil, errSessionExpired
return nil, ErrSessionExpired
}
return session, nil
}


+ 8
- 0
themes/alps/assets/compose.js View File

@@ -1,3 +1,11 @@
const textarea = document.querySelector("textarea.body");
if (window.location.pathname.endsWith("/reply")) {
// Auto-focus body and scroll to bottom
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
textarea.scrollTop = textarea.scrollHeight;
}

const sendButton = document.getElementById("send-button"),
saveButton = document.getElementById("save-button");



BIN
themes/alps/assets/favicon-128.png View File

Before After
Width: 128  |  Height: 128  |  Size: 5.1 KiB

BIN
themes/alps/assets/favicon-16x16.png View File

Before After
Width: 16  |  Height: 16  |  Size: 561 B

BIN
themes/alps/assets/favicon-196x196.png View File

Before After
Width: 196  |  Height: 196  |  Size: 23 KiB

BIN
themes/alps/assets/favicon-32x32.png View File

Before After
Width: 32  |  Height: 32  |  Size: 1.1 KiB

BIN
themes/alps/assets/favicon-96x96.png View File

Before After
Width: 96  |  Height: 96  |  Size: 5.3 KiB

+ 29
- 4
themes/alps/assets/style.css View File

@@ -88,7 +88,8 @@ input[type="file"],
input[type="number"],
input[type="date"],
input[type="time"],
textarea {
textarea,
select {
margin: 0;
border: none;
border: 1px solid #e0e0e0;
@@ -118,13 +119,31 @@ header nav div { float: right; }
header nav div > a{ margin-left: 1rem; }
header a.active { font-weight: bold; color: black; text-decoration: none; }

header .notice {
color: #0c5460;
background-color: #d1ecf1;
border: 1px solid #bee5eb;
padding: 0.5rem;
text-align: center;
}

footer { text-align: right; }

.actions { padding: 0.5rem; }

.container { flex: 1 auto; display: flex; flex-direction: column; flex-wrap: nowrap; min-width: 0; }
.container {
flex: 1 auto;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-width: 0;
}

.container.error {
max-width: 800px;
margin: 0 auto;
padding: 1rem 0;
}

aside { flex: 0 0 180px; }

@@ -139,7 +158,7 @@ aside ul {
aside li {
width: 100%;
display: flex;
padding: 0.4rem 0 0.4rem 0.5rem;
padding: 0.4rem 0.5rem;
}

aside li a {
@@ -635,7 +654,9 @@ main table tfoot {
}

.action-group label,
.action-group input {
.action-group input,
.action-group textarea,
.action-group select {
display: block;
width: 100%;
}
@@ -646,6 +667,10 @@ main table tfoot {
float: left;
}

.action-group select {
height: 10rem;
}

.actions-message,
.actions-contacts {
display: flex;


+ 2
- 4
themes/alps/compose.html View File

@@ -14,7 +14,7 @@

<div class="headers no-js">
<label>From</label>
<input type="email" name="from" id="from" value="{{.Message.From}}" />
<input type="text" name="from" id="from" value="{{.Message.From}}" />

<label>To</label>
{{ $to := .Message.ToString }}
@@ -22,12 +22,10 @@
{{ $to = "" }}
{{ end }}
<input
type="email"
type="text"
name="to"
id="to"
value="{{$to}}"
multiple
list="emails"
{{ if not $to }} autofocus{{ end }}
/>



+ 14
- 0
themes/alps/error.html View File

@@ -0,0 +1,14 @@
{{template "head.html" .}}

<div class="page-wrap">
<div class="container error">
<h1>{{.Code}}: {{.Status}}</h1>
<p>
An error occured. You can try
<a href="/">returning to your inbox</a>,
or contact support.
</p>
</div>
</div>

{{template "foot.html"}}

+ 5
- 0
themes/alps/head.html View File

@@ -10,5 +10,10 @@
<title>{{.GlobalData.Title}}</title>
<link rel="stylesheet" href="/themes/alps/assets/style.css">
<link rel="stylesheet" href="/themes/alps/assets/print.css" media="print">
<link rel="icon" type="image/png" href="/themes/alps/assets/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="/themes/alps/assets/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="/themes/alps/assets/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/themes/alps/assets/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="/themes/alps/assets/favicon-128.png" sizes="128x128" />
</head>
<body>

+ 1
- 11
themes/alps/mailbox.html View File

@@ -21,7 +21,7 @@
{{ $classes = printf "%s %s" $classes "message-list-deleted" }}
{{ end }}

{{ if not (.HasFlag "\\Deleted") }}
{{ if and (not (.HasFlag "\\Deleted")) .Envelope }}
<div class="message-list-checkbox {{$classes}}">
<input type="checkbox" name="uids" value="{{.Uid}}" form="messages-form">
</div>
@@ -70,16 +70,6 @@
<div class="message-list-date {{$classes}}">
{{ .Envelope.Date | humantime }}
</div>
{{ else }}
<div class="message-list-checkbox {{$classes}}">
<input type="checkbox" form="messages-form" disabled readonly>
</div>
<div class="message-list-sender {{$classes}}"></div>
<div class="message-list-flags {{$classes}}"></div>
<div class="message-list-subject {{$classes}}">
<em>(this email was deleted by another client)</em>
</div>
<div class="message-list-date {{$classes}}"></div>
{{ end }}

{{ end }}


+ 7
- 1
themes/alps/message.html View File

@@ -190,7 +190,13 @@
{{ if eq $text.PathString .Part.PathString }}
class="active"
{{ end }}
>Plain text</a>
>
{{ if eq $text.MIMEType "text/html" }}
HTML
{{ else }}
Plain text
{{ end }}
</a>
{{ if and $html $text }}
{{ if ne $html.PathString $text.PathString }}
<a


+ 8
- 0
themes/alps/nav.html View File

@@ -22,10 +22,18 @@
{{ end }}
>Contacts</a>
{{ end }}
{{ if .GlobalData.LoggedIn }}
<div>
<span>{{ .GlobalData.Username }}</span>
<a href="/settings">Settings</a>
<a href="/logout">Sign Out</a>
</div>
{{ end }}
</nav>
{{ if .GlobalData.Notice }}
<div class="notice">
{{ .GlobalData.Notice }}
<a href="{{.GlobalData.URL.String}}">Dismiss</a>
</div>
{{ end }}
</header>

+ 41
- 1
themes/alps/settings.html View File

@@ -3,13 +3,53 @@

<div class="page-wrap">
<aside>
<a href="/mailbox/INBOX">Back to inbox</a>
<ul>
<li>
<a href="/mailbox/INBOX">« Back to inbox</a>
</li>
</ul>
</aside>

<div class="container">
<main class="settings">
<form method="post">
<div class="action-group">
<label for="from">Full name</label>
<input
type="text"
name="from"
id="from"
value="{{.Settings.From}}"
/>
</div>

<div class="action-group">
<label for="signature">Message signature</label>
<textarea
name="signature"
id="signature"
rows="5"
>{{.Settings.Signature}}</textarea>
</div>

<div class="action-group">
<label for="subscriptions">Subscribed folders</label>
<select name="subscriptions" id="subscriptions" multiple>
{{ $subs := .Subscriptions }}
{{ range .Mailboxes }}
{{ if and (ne .Name "INBOX") (not (.HasAttr "\\Noselect")) }}
<option
value="{{.Name}}"
{{ if $subs.Has .Name }}
selected
{{ end }}
>{{.Name}}</option>
{{ end }}
{{ end }}
</select>
</div>

<div class="action-group">
<label for="messages_per_page">Messages per page</label>
<input
type="number"


+ 11
- 25
themes/alps/util.html View File

@@ -1,42 +1,28 @@
{{ define "mbox-link" }}
{{ if not (.HasAttr "\\Noselect") }}
<li {{ if .Active }}class="active"{{ end }}>
<a href="{{.URL}}">
{{- if eq .Name "INBOX" -}}
{{ if not (.Info.HasAttr "\\Noselect") }}
<li {{ if .Info.Active }}class="active"{{ end }}>
<a href="{{.Info.URL}}">
{{- if eq .Info.Name "INBOX" -}}
Inbox
{{- else -}}
{{ .Name }}
{{ .Info.Name }}
{{- end -}}
{{- if .HasAttr "\\HasChildren" }}/{{ end }}
{{- if .Info.HasAttr "\\HasChildren" }}/{{ end }}
</a>
<!-- TODO: Rig up this form -->
<button
type="submit"
name="subscribe"
value="{{.Name}}"
form="subscribe-form"
{{ if eq .Name "INBOX" }}
title="Unsubscribe"
{{ else }}
title="Subscribe"
{{ end }}
>
{{ if eq .Name "INBOX" }}
{{ else }}
{{ if .Status }}
{{ if .Status.Unseen }}
<span class="unseen">({{.Status.Unseen}})</span>
{{ end }}
{{ end }}
</button>
</li>
{{ else }}
<li class="noselect">
{{.Name}}{{- if .HasAttr "\\HasChildren" }}/{{ end }}
{{.Info.Name}}{{- if .Info.HasAttr "\\HasChildren" }}/{{ end }}
</li>
{{ end }}
{{ end }}

{{ define "aside" }}
<form id="subscribe-form" method="POST"></form>
<aside>
<ul>
<!-- the logo image, dimensions 200x32 may be present or not -->


Loading…
Cancel
Save