From 1552da11ab868537824e551530d88f68d4bab9ee Mon Sep 17 00:00:00 2001 From: nahakubuilder Date: Sun, 30 Aug 2026 19:10:04 +0100 Subject: [PATCH] notifications and update layout for small screen --- .gitignore | 20 +++++- cmd/server/main.go | 25 ++++++++ config/config.go | 37 ++++++++++- go.mod | 2 + go.sum | 66 ++++++++++++++++++++ internal/db/db.go | 82 +++++++++++++++++++++++++ internal/handlers/push.go | 60 ++++++++++++++++++ internal/syncer/syncer.go | 69 +++++++++++++++++++++ web/static/css/gowebmail.css | 59 ++++++++++++++++-- web/static/icons/icon-192.png | Bin 0 -> 11349 bytes web/static/icons/icon-512-maskable.png | Bin 0 -> 28617 bytes web/static/icons/icon-512.png | Bin 0 -> 17439 bytes web/static/js/app.js | 65 +++++++++++++++++++- web/static/js/gowebmail.js | 5 ++ web/static/manifest.json | 15 +++++ web/static/sw.js | 42 +++++++++++++ web/templates/app.html | 33 ++++++++-- web/templates/base.html | 3 + webfs.go | 2 +- 19 files changed, 567 insertions(+), 18 deletions(-) create mode 100644 internal/handlers/push.go create mode 100644 web/static/icons/icon-192.png create mode 100644 web/static/icons/icon-512-maskable.png create mode 100644 web/static/icons/icon-512.png create mode 100644 web/static/manifest.json create mode 100644 web/static/sw.js diff --git a/.gitignore b/.gitignore index 35f2a56..ca161d8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,22 @@ webmail.code-workspace graphify-out GEMINI.md -tests/ \ No newline at end of file +tests/ + +android/build/ +android/app/build/ +android/app/release/ +android/.gradle/ +android/.idea/ +android/.kotlin/ +android/captures/ +android/*.iml +android/app/*.iml +android/local.properties +android/keystore.properties +android/*.keystore +android/*.jks +android/*.apk +android/*.aab +android/**/.cxx/ +android/**/.externalNativeBuild/ \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 4b4e91b..4ccfc3f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -143,6 +143,26 @@ func main() { w.Header().Set("Content-Type", "image/png") w.Write(data) }) + // PWA manifest + service worker — served at root (not /static/) so the service worker's + // default scope covers the whole app, not just /static/. + r.HandleFunc("/manifest.json", func(w http.ResponseWriter, r *http.Request) { + data, err := gowebmail.WebFS.ReadFile("web/static/manifest.json") + if err != nil { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/manifest+json") + w.Write(data) + }) + r.HandleFunc("/sw.js", func(w http.ResponseWriter, r *http.Request) { + data, err := gowebmail.WebFS.ReadFile("web/static/sw.js") + if err != nil { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/javascript") + w.Write(data) + }) // Public auth routes auth := r.PathPrefix("/auth").Subrouter() auth.HandleFunc("/login", h.Auth.ShowLogin).Methods("GET") @@ -278,6 +298,11 @@ func main() { api.HandleFunc("/accounts/sort-order", h.API.SetAccountSortOrder).Methods("PUT") api.HandleFunc("/ui-prefs", h.API.GetUIPrefs).Methods("GET") api.HandleFunc("/ui-prefs", h.API.SetUIPrefs).Methods("PUT") + + // Web Push (background new-mail notifications) + api.HandleFunc("/push/vapid-public-key", h.API.GetVAPIDPublicKey).Methods("GET") + api.HandleFunc("/push/subscribe", h.API.SubscribePush).Methods("POST") + api.HandleFunc("/push/unsubscribe", h.API.UnsubscribePush).Methods("POST") api.HandleFunc("/login-history", h.API.ListMyLoginHistory).Methods("GET") // Search diff --git a/config/config.go b/config/config.go index dac410c..3929e41 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,8 @@ import ( "os" "strconv" "strings" + + webpush "github.com/SherClockHolmes/webpush-go" ) // Config holds all application configuration. @@ -61,6 +63,10 @@ type Config struct { MicrosoftClientSecret string MicrosoftTenantID string MicrosoftRedirectURL string // auto-derived from BaseURL if blank + + // Web Push (VAPID) — signs background push notifications to browsers/mobile PWAs + VAPIDPublicKey string + VAPIDPrivateKey string } const configPath = "./data/gowebmail.conf" @@ -325,6 +331,20 @@ var allFields = []configField{ "Must exactly match what is registered in Azure.", }, }, + { + key: "VAPID_PUBLIC_KEY", + defVal: "", + comments: []string{ + "--- Web Push Notifications ---", + "VAPID keypair signing background push notifications (browser + mobile PWA/Android wrapper).", + "Auto-generated on first run. Do not edit manually.", + }, + }, + { + key: "VAPID_PRIVATE_KEY", + defVal: "", + comments: []string{}, + }, } // Load reads/creates data/gowebmail.conf, fills in missing keys, then returns Config. @@ -347,6 +367,14 @@ func Load() (*Config, error) { if existing["SESSION_SECRET"] == "" { existing["SESSION_SECRET"] = mustHex(32) } + if existing["VAPID_PUBLIC_KEY"] == "" || existing["VAPID_PRIVATE_KEY"] == "" { + priv, pub, err := webpush.GenerateVAPIDKeys() + if err != nil { + return nil, fmt.Errorf("generate VAPID keys: %w", err) + } + existing["VAPID_PRIVATE_KEY"] = priv + existing["VAPID_PUBLIC_KEY"] = pub + } // Write back (preserves existing, adds any new fields from allFields) if err := writeConfigFile(configPath, existing); err != nil { @@ -463,6 +491,9 @@ func Load() (*Config, error) { MicrosoftClientSecret: get("MICROSOFT_CLIENT_SECRET"), MicrosoftTenantID: orDefault(get("MICROSOFT_TENANT_ID"), "consumers"), MicrosoftRedirectURL: outlookRedirect, + + VAPIDPublicKey: get("VAPID_PUBLIC_KEY"), + VAPIDPrivateKey: get("VAPID_PRIVATE_KEY"), } // Derive SECURE_COOKIE automatically if BASE_URL uses https @@ -652,8 +683,10 @@ func writeConfigFile(path string, values map[string]string) error { // SESSION_SECRET and ENCRYPTION_KEY are intentionally excluded. var EditableKeys = func() map[string]bool { excluded := map[string]bool{ - "SESSION_SECRET": true, - "ENCRYPTION_KEY": true, + "SESSION_SECRET": true, + "ENCRYPTION_KEY": true, + "VAPID_PUBLIC_KEY": true, + "VAPID_PRIVATE_KEY": true, } m := map[string]bool{} for _, f := range allFields { diff --git a/go.mod b/go.mod index 4ba428f..4aa6778 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,10 @@ require ( require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect + github.com/SherClockHolmes/webpush-go v1.4.0 // indirect github.com/cloudflare/circl v1.6.2 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/teambition/rrule-go v1.8.2 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect diff --git a/go.sum b/go.sum index bad5ba1..e30809c 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= +github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM= @@ -16,24 +18,88 @@ github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHC github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-webdav v0.7.0 h1:cp6aBWXBf8Sjzguka9VJarr4XTkGc2IHxXI1Gq3TKpA= github.com/emersion/go-webdav v0.7.0/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w= github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8= github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mozilla.org/pkcs7 v0.10.0 h1:jmljzDzNYFzaP1dFlgmCiQml9e+iEMmv8/NNs4evQbg= go.mozilla.org/pkcs7 v0.10.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU= software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/db/db.go b/internal/db/db.go index dc18b2a..48032cb 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -494,6 +494,23 @@ func (d *DB) Migrate() error { // One-row-per-migration marker table so a one-time data backfill (as opposed to a // schema ALTER, which is naturally idempotent) runs exactly once, ever — never // re-applying and silently overwriting a choice the user made after that first run. + // Web Push subscriptions — a browser/WebView calls PushManager.subscribe() once per + // device and posts the result here; the syncer looks these up by user_id to deliver + // background new-mail notifications via VAPID. Outlives login sessions on purpose. + if _, err := d.sql.Exec(`CREATE TABLE IF NOT EXISTS push_subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + endpoint TEXT NOT NULL UNIQUE, + p256dh TEXT NOT NULL, + auth TEXT NOT NULL, + created_at DATETIME DEFAULT (datetime('now')) + )`); err != nil { + return fmt.Errorf("create push_subscriptions: %w", err) + } + if _, err := d.sql.Exec(`CREATE INDEX IF NOT EXISTS idx_push_subs_user ON push_subscriptions(user_id)`); err != nil { + return fmt.Errorf("create idx_push_subs_user: %w", err) + } + if _, err := d.sql.Exec(`CREATE TABLE IF NOT EXISTS data_migrations ( name TEXT PRIMARY KEY, applied_at DATETIME DEFAULT (datetime('now')) @@ -3634,3 +3651,68 @@ func (d *DB) GetTrustedCert(accountID int64, fingerprint string) (string, error) } return certPEM, err } + +// ---- Web Push subscriptions ---- + +// PushSubscription is a decrypted row from push_subscriptions, shaped to match +// webpush.Subscription directly (Endpoint + Keys.P256dh/Auth). +type PushSubscription struct { + ID int64 + Endpoint string + P256dh string + Auth string +} + +// UpsertPushSubscription stores (or refreshes) a browser's PushSubscription for userID. +// p256dh/auth are encrypted at rest like other sensitive fields. +func (d *DB) UpsertPushSubscription(userID int64, endpoint, p256dh, auth string) error { + encP256dh, err := d.enc.Encrypt(p256dh) + if err != nil { + return err + } + encAuth, err := d.enc.Encrypt(auth) + if err != nil { + return err + } + _, err = d.sql.Exec(` + INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth) + VALUES (?, ?, ?, ?) + ON CONFLICT(endpoint) DO UPDATE SET user_id=excluded.user_id, p256dh=excluded.p256dh, auth=excluded.auth`, + userID, endpoint, encP256dh, encAuth) + return err +} + +// DeletePushSubscription removes a subscription by its endpoint, scoped to userID so one +// user can't unsubscribe another's device. +func (d *DB) DeletePushSubscription(userID int64, endpoint string) error { + _, err := d.sql.Exec(`DELETE FROM push_subscriptions WHERE user_id=? AND endpoint=?`, userID, endpoint) + return err +} + +// DeletePushSubscriptionByEndpoint removes a subscription regardless of owner — used when +// the push service reports the endpoint is gone (410/404), so no user_id is known at that point. +func (d *DB) DeletePushSubscriptionByEndpoint(endpoint string) error { + _, err := d.sql.Exec(`DELETE FROM push_subscriptions WHERE endpoint=?`, endpoint) + return err +} + +// GetPushSubscriptionsForUser returns every device subscribed to push for userID. +func (d *DB) GetPushSubscriptionsForUser(userID int64) ([]*PushSubscription, error) { + rows, err := d.sql.Query(`SELECT id, endpoint, p256dh, auth FROM push_subscriptions WHERE user_id=?`, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []*PushSubscription + for rows.Next() { + var s PushSubscription + var encP256dh, encAuth string + if err := rows.Scan(&s.ID, &s.Endpoint, &encP256dh, &encAuth); err != nil { + continue + } + s.P256dh, _ = d.enc.Decrypt(encP256dh) + s.Auth, _ = d.enc.Decrypt(encAuth) + out = append(out, &s) + } + return out, rows.Err() +} diff --git a/internal/handlers/push.go b/internal/handlers/push.go new file mode 100644 index 0000000..f0d0e65 --- /dev/null +++ b/internal/handlers/push.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "encoding/json" + "io" + "net/http" + + webpush "github.com/SherClockHolmes/webpush-go" + "github.com/ghostersk/gowebmail/internal/middleware" +) + +// GetVAPIDPublicKey exposes the server's VAPID public key so the frontend can pass it to +// PushManager.subscribe({applicationServerKey: ...}). +func (h *APIHandler) GetVAPIDPublicKey(w http.ResponseWriter, r *http.Request) { + h.writeJSON(w, map[string]string{"public_key": h.cfg.VAPIDPublicKey}) +} + +// SubscribePush stores a browser/WebView's PushSubscription (from +// PushManager.subscribe().toJSON()) so background new-mail push can reach it. +func (h *APIHandler) SubscribePush(w http.ResponseWriter, r *http.Request) { + userID := middleware.GetUserID(r) + body, err := io.ReadAll(io.LimitReader(r.Body, 8*1024)) + if err != nil { + h.writeError(w, http.StatusBadRequest, "invalid request") + return + } + var sub webpush.Subscription + if err := json.Unmarshal(body, &sub); err != nil || sub.Endpoint == "" || sub.Keys.P256dh == "" || sub.Keys.Auth == "" { + h.writeError(w, http.StatusBadRequest, "invalid push subscription") + return + } + if err := h.db.UpsertPushSubscription(userID, sub.Endpoint, sub.Keys.P256dh, sub.Keys.Auth); err != nil { + h.writeError(w, http.StatusInternalServerError, "failed to save subscription") + return + } + h.writeJSON(w, map[string]bool{"ok": true}) +} + +// UnsubscribePush removes a subscription by endpoint (sent when the user disables the +// notifications toggle, or the browser reports the subscription changed/expired). +func (h *APIHandler) UnsubscribePush(w http.ResponseWriter, r *http.Request) { + userID := middleware.GetUserID(r) + body, err := io.ReadAll(io.LimitReader(r.Body, 8*1024)) + if err != nil { + h.writeError(w, http.StatusBadRequest, "invalid request") + return + } + var req struct { + Endpoint string `json:"endpoint"` + } + if err := json.Unmarshal(body, &req); err != nil || req.Endpoint == "" { + h.writeError(w, http.StatusBadRequest, "endpoint required") + return + } + if err := h.db.DeletePushSubscription(userID, req.Endpoint); err != nil { + h.writeError(w, http.StatusInternalServerError, "failed to remove subscription") + return + } + h.writeJSON(w, map[string]bool{"ok": true}) +} diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go index a552e6b..b1923b7 100644 --- a/internal/syncer/syncer.go +++ b/internal/syncer/syncer.go @@ -7,12 +7,14 @@ package syncer import ( "context" + "encoding/json" "fmt" "log" "strings" "sync" "time" + webpush "github.com/SherClockHolmes/webpush-go" "github.com/ghostersk/gowebmail/internal/logger" "github.com/ghostersk/gowebmail/config" "github.com/ghostersk/gowebmail/internal/auth" @@ -495,6 +497,7 @@ func (s *Scheduler) syncFolder(c *email.Client, account *models.EmailAccount, db } // 1. Fetch new messages (UID > lastSeenUID) + isIncrementalSync := lastSeenUID != 0 // false = first-ever sync or post-UIDVALIDITY full re-sync var msgs []*models.Message if lastSeenUID == 0 { // First sync: respect the account's days/all setting @@ -510,6 +513,11 @@ func (s *Scheduler) syncFolder(c *email.Client, account *models.EmailAccount, db return 0, fmt.Errorf("fetch new: %w", err) } + // Collected only for a genuine incremental inbox sync — never for first-sync/full-resync + // backfill (that's historical mail, not "new mail") — mirrors the same restraint the + // reconciliation step below already applies to rules/spam-move side effects. + var pushCandidates []*models.Message + maxUID := lastSeenUID for _, msg := range msgs { msg.FolderID = dbFolder.ID @@ -523,6 +531,8 @@ func (s *Scheduler) syncFolder(c *email.Client, account *models.EmailAccount, db s.moveToSpamIMAP(account, dbFolder, msg) } else if rule := matchRule(msg, account.EmailAddress, activeRules); rule != nil { s.applyRuleIMAP(c, account, dbFolder, msg, rule) + } else if isIncrementalSync && dbFolder.FolderType == "inbox" && !msg.IsRead { + pushCandidates = append(pushCandidates, msg) } } uid := uint32(0) @@ -531,6 +541,9 @@ func (s *Scheduler) syncFolder(c *email.Client, account *models.EmailAccount, db maxUID = uid } } + if len(pushCandidates) > 0 { + s.sendNewMailPush(account.UserID, pushCandidates) + } // 2. Sync flags for ALL existing messages (catch read/star changes from other clients) flags, err := c.SyncFlags(dbFolder.FullPath) @@ -601,6 +614,62 @@ func (s *Scheduler) syncFolder(c *email.Client, account *models.EmailAccount, db return newMessages, nil } +// sendNewMailPush delivers a background Web Push notification for genuinely new unread +// inbox mail to every device userID has subscribed (Settings > General > Notifications). +// Mirrors the title/body convention already used client-side by sendOSNotification() in +// app.js so a background push and a foreground toast read the same way. +func (s *Scheduler) sendNewMailPush(userID int64, msgs []*models.Message) { + if s.cfg.VAPIDPrivateKey == "" || s.cfg.VAPIDPublicKey == "" { + return + } + subs, err := s.db.GetPushSubscriptionsForUser(userID) + if err != nil || len(subs) == 0 { + return + } + + first := msgs[0] + fromLabel := first.FromName + if fromLabel == "" { + fromLabel = first.FromEmail + } + subject := first.Subject + if subject == "" { + subject = "(no subject)" + } + + title, body := fromLabel, subject + if len(msgs) > 1 { + title = fmt.Sprintf("%d new messages in GoWebMail", len(msgs)) + body = fmt.Sprintf("%s: %s", fromLabel, subject) + } + + payload, err := json.Marshal(map[string]string{"title": title, "body": body, "tag": "gowebmail-new"}) + if err != nil { + return + } + + for _, sub := range subs { + resp, err := webpush.SendNotification(payload, &webpush.Subscription{ + Endpoint: sub.Endpoint, + Keys: webpush.Keys{P256dh: sub.P256dh, Auth: sub.Auth}, + }, &webpush.Options{ + Subscriber: "mailto:noreply@" + s.cfg.Hostname, + VAPIDPublicKey: s.cfg.VAPIDPublicKey, + VAPIDPrivateKey: s.cfg.VAPIDPrivateKey, + TTL: 60, + }) + if err != nil { + log.Printf("[push] send to user %d: %v", userID, err) + continue + } + resp.Body.Close() + if resp.StatusCode == 404 || resp.StatusCode == 410 { + // Subscription is dead (browser data cleared, app uninstalled, etc). + s.db.DeletePushSubscriptionByEndpoint(sub.Endpoint) + } + } +} + // ---- Pending ops drain ---- // Applies queued IMAP write operations (delete/move/flag) with retry logic. diff --git a/web/static/css/gowebmail.css b/web/static/css/gowebmail.css index 858358f..ee24a10 100644 --- a/web/static/css/gowebmail.css +++ b/web/static/css/gowebmail.css @@ -349,9 +349,9 @@ body.app-page{overflow:hidden} .detail-from strong{color:var(--text)} .detail-from span{color:var(--muted);font-size:12px} .detail-date{font-size:12px;color:var(--muted);flex-shrink:0} -.detail-actions{padding:6px 20px;border-bottom:1px solid var(--border);display:flex;gap:6px;flex-shrink:0} -.action-btn{padding:4px 10px;background:var(--surface2);border:1px solid var(--border2);border-radius:5px; - color:var(--text2);font-family:'DM Sans',sans-serif;font-size:12px;cursor:pointer;transition:background .15s} +.detail-actions{padding:6px 20px;border-bottom:1px solid var(--border);display:flex;gap:5px;flex-shrink:0} +.action-btn{padding:3px 8px;background:var(--surface2);border:1px solid var(--border2);border-radius:5px; + color:var(--text2);font-family:'DM Sans',sans-serif;font-size:11px;cursor:pointer;transition:background .15s} .action-btn:hover{background:var(--surface3);color:var(--text)} .action-btn.danger:hover{background:rgba(239,68,68,.1);color:var(--danger);border-color:rgba(239,68,68,.3)} .detail-body{flex:1;overflow-y:auto;padding:16px 20px} @@ -474,6 +474,10 @@ body.admin-page{overflow:auto;background:var(--bg)} .fmt-btn{background:none;border:none;color:var(--text2);cursor:pointer;padding:4px 7px;border-radius:4px;font-size:13px;line-height:1;transition:background .1s} .fmt-btn:hover{background:var(--border2);color:var(--text)} .fmt-sep{width:1px;height:16px;background:var(--border2);margin:0 3px} +.fmt-font-select{background:var(--surface2);border:1px solid var(--border2);border-radius:4px;color:var(--text); + font-size:12px;padding:3px 4px;cursor:pointer;max-width:130px;color-scheme:dark} +.fmt-font-select:hover{border-color:var(--accent)} +.fmt-font-select option{background:var(--surface2);color:var(--text)} .compose-editor{flex:1;overflow-y:auto;padding:10px 12px; font-size:13px;line-height:1.6;color:var(--text);outline:none;background:var(--bg);min-height:0} .compose-editor:empty::before{content:attr(placeholder);color:var(--muted);pointer-events:none} @@ -519,6 +523,9 @@ body.admin-page{overflow:auto;background:var(--bg)} padding:4px 6px;min-height:32px;cursor:text;background:transparent} .tag-container:focus-within{} .compose-tag-field label{flex-shrink:0;align-self:flex-start;padding-top:7px} +.compose-cc-bcc-toggle{display:flex;gap:8px;flex-shrink:0;align-self:flex-start;padding-top:7px} +.cc-bcc-btn{background:none;border:none;color:var(--muted);cursor:pointer;font-size:12px;padding:0;transition:color .15s} +.cc-bcc-btn:hover{color:var(--accent)} .email-tag{display:inline-flex;align-items:center;gap:3px;padding:2px 6px 2px 8px; background:var(--surface3);border:1px solid var(--border2);border-radius:12px; font-size:12px;color:var(--text);white-space:nowrap;max-width:260px} @@ -686,8 +693,21 @@ body.admin-page{overflow:auto;background:var(--bg)} /* Desktop-only sidebar collapse control — mobile already has the drawer/hamburger */ .sidebar-collapse-btn{display:none} - /* Message list panel: full width, shown/hidden by data-mob-view */ - .message-list-panel{width:100%;border-right:none;flex-shrink:0} + /* Message action row (Reply/Forward/Star/...): wrap instead of overflowing + horizontally — there's no room for 9+ buttons in one row on a phone. */ + .detail-actions{flex-wrap:wrap} + + /* Let the whole message view (subject/from/buttons + body) scroll together as one + on a phone, instead of pinning the header/action row in place and squeezing the + body into whatever vertical space is left over — that left almost no room to read. */ + .message-detail{overflow-y:auto} + .detail-body{flex:none;overflow-y:visible} + + /* Message list panel: full width/height, shown/hidden by data-mob-view. !important + because the desktop drag-resize / reading-pane-bottom feature can leave an inline + height (e.g. "35%") on this element from a persisted desktop layout — without it, + the list would only fill that leftover fraction of the screen on mobile. */ + .message-list-panel{width:100%!important;height:100%!important;border-right:none;flex-shrink:0} .message-detail{width:100%} /* View switching via data-mob-view on #app-root */ @@ -705,6 +725,35 @@ body.admin-page{overflow:auto;background:var(--bg)} } /* Hide floating minimised bar on mobile, use back button instead */ .compose-minimised{display:none!important} + + /* Any other modal (add/edit account, login history, spam block, etc.): fit the screen + instead of overflowing a fixed desktop width. */ + .modal{width:calc(100vw - 24px)!important;max-width:440px} + + /* Settings modal on mobile becomes a full-screen "page" — it must render above the + app's own fixed .mob-topbar (z-index:200), otherwise the topbar sits on top of the + modal's own close button and swallows the tap. Modals opened from inside Settings + (add/edit account, etc.) keep stacking above it, just shifted up to match. */ + #settings-modal{z-index:220} + #add-account-modal,#edit-account-modal,#login-history-modal,#spam-block-modal{z-index:230} + + /* Settings modal: full screen, side nav becomes a horizontal sliding tab strip on top. */ + .settings-modal-box{ + width:100vw!important;max-width:100vw!important;height:100dvh!important;height:100vh!important; + max-height:100vh!important;border-radius:0!important; + position:fixed!important;inset:0!important; + } + .settings-body{flex-direction:column!important} + .settings-nav{ + width:100%!important;flex-direction:row!important;flex-shrink:0; + overflow-x:auto;-webkit-overflow-scrolling:touch; + border-right:none!important;border-bottom:1px solid var(--border); + padding:8px 10px!important;gap:4px!important; + } + .settings-nav button{ + width:auto!important;display:inline-block;white-space:nowrap;flex-shrink:0; + padding:6px 12px;font-size:12px; + } } /* ── Contacts ──────────────────────────────────────────────────────────── */ diff --git a/web/static/icons/icon-192.png b/web/static/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..1594fde2cb69d2dad7aef6ad43063f423a7e4e98 GIT binary patch literal 11349 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 X1_D~a$}lfb00000NkvXXu0mjf)Ihoy literal 0 HcmV?d00001 diff --git a/web/static/icons/icon-512-maskable.png b/web/static/icons/icon-512-maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f8b9fd08f3c4eb3827380cf2a9155695a782a7 GIT binary patch literal 28617 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 literal 0 HcmV?d00001 diff --git a/web/static/icons/icon-512.png b/web/static/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b04ad99ecd987cd1701d798a2d0b3292cfa817 GIT binary patch literal 17439 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 G_x}Ou$b9Gk literal 0 HcmV?d00001 diff --git a/web/static/js/app.js b/web/static/js/app.js index 31a5397..11e1523 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1786,7 +1786,6 @@ function renderMessageDetail(msg, showRemoteContent) { ${threadBtnHtml} - @@ -2031,6 +2030,8 @@ function openCompose(opts={}) { document.getElementById('compose-subject').value=opts.subject||''; document.getElementById('cc-row').style.display='none'; document.getElementById('bcc-row').style.display='none'; + document.getElementById('cc-toggle-btn').style.display=''; + document.getElementById('bcc-toggle-btn').style.display=''; populateComposeFrom(opts.accountId||null); const editor=document.getElementById('compose-editor'); const fromAccountId=parseInt(document.getElementById('compose-from')?.value||0); @@ -2092,8 +2093,8 @@ function _closeCompose() { S.composeVisible=false; S.composeMinimised=false; S.draftDirty=false; } -function showCCRow() { document.getElementById('cc-row').style.display='flex'; } -function showBCCRow() { document.getElementById('bcc-row').style.display='flex'; } +function showCCRow() { document.getElementById('cc-row').style.display='flex'; document.getElementById('cc-toggle-btn').style.display='none'; } +function showBCCRow() { document.getElementById('bcc-row').style.display='flex'; document.getElementById('bcc-toggle-btn').style.display='none'; } function openReply() { if (S.currentMessage) openReplyTo(S.currentMessage.id); } @@ -2302,6 +2303,22 @@ async function discardDraft() { // ── Compose formatting ───────────────────────────────────────────────────── function execFmt(cmd,val) { document.getElementById('compose-editor').focus(); document.execCommand(cmd,false,val||null); } + +// Opening the native -
+
+
+ + +
+