16 コミット

作成者 SHA1 メッセージ 日付
  j3s b5fcf10c44 Repoint repository references to new location 3年前
  Drew DeVault 51d762ac5f Implement mailbox subscriptions 3年前
  Drew DeVault 8cc742f45d Fix issues with to/from headers 3年前
  Drew DeVault 61cdb93e48 s/email/text/ in To & From fields 3年前
  Drew DeVault 797b426ec6 Add "Full name" option to settings 3年前
  Drew DeVault 51498a2dc3 JS enhancements to encourage bottom-posting 3年前
  Drew DeVault a1e8bcc561 Implement message signature setting 3年前
  Drew DeVault a5d2af2c4e Remove "this email was deleted by another client" 3年前
  Drew DeVault cb37df882e Add notices on action completion 3年前
  Drew DeVault 405c18d213 Convert HTML to plaintext for forwarding & replies 3年前
  Drew DeVault 2fb78cad97 Fix name of HTML tab in HTML-only emails 3年前
  Drew DeVault 2430473dbc Comment out subscription buttons 3年前
  Drew DeVault 8771ddeb2d Add favicons for alps theme 3年前
  Drew DeVault 1992880454 Add theme-specific error page 3年前
  Drew DeVault 5087e4b327 Improve "attachments exceed max size" error message 3年前
  Drew DeVault 297afc5ce6 Limit total size of unsent attachments 3年前
41個のファイルの変更420行の追加148行の削除
分割表示
  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. バイナリ
      themes/alps/assets/favicon-128.png
  29. バイナリ
      themes/alps/assets/favicon-16x16.png
  30. バイナリ
      themes/alps/assets/favicon-196x196.png
  31. バイナリ
      themes/alps/assets/favicon-32x32.png
  32. バイナリ
      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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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");



バイナリ
themes/alps/assets/favicon-128.png ファイルの表示

変更前 変更後
幅: 128  |  高さ: 128  |  サイズ: 5.1 KiB

バイナリ
themes/alps/assets/favicon-16x16.png ファイルの表示

変更前 変更後
幅: 16  |  高さ: 16  |  サイズ: 561 B

バイナリ
themes/alps/assets/favicon-196x196.png ファイルの表示

変更前 変更後
幅: 196  |  高さ: 196  |  サイズ: 23 KiB

バイナリ
themes/alps/assets/favicon-32x32.png ファイルの表示

変更前 変更後
幅: 32  |  高さ: 32  |  サイズ: 1.1 KiB

バイナリ
themes/alps/assets/favicon-96x96.png ファイルの表示

変更前 変更後
幅: 96  |  高さ: 96  |  サイズ: 5.3 KiB

+ 29
- 4
themes/alps/assets/style.css ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 -->


読み込み中…
キャンセル
保存