1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-04-25 12:25:04 +02:00

UI: Add about app modal with version update notification

This commit is contained in:
Ralph Slooten 2022-10-08 23:23:30 +13:00
parent 675704ca91
commit a31a7c3d2c
10 changed files with 146 additions and 67 deletions

View File

@ -42,4 +42,4 @@ jobs:
asset_name: mailpit-${{ matrix.goos }}-${{ matrix.goarch }} asset_name: mailpit-${{ matrix.goos }}-${{ matrix.goarch }}
extra_files: LICENSE README.md extra_files: LICENSE README.md
md5sum: false md5sum: false
ldflags: -w -X "github.com/axllent/mailpit/cmd.Version=${{ steps.tag.outputs.tag }}" ldflags: -w -X "github.com/axllent/mailpit/config.Version=${{ steps.tag.outputs.tag }}"

View File

@ -8,7 +8,7 @@ WORKDIR /app
RUN apk add --no-cache git npm && \ RUN apk add --no-cache git npm && \
npm install && npm run package && \ npm install && npm run package && \
CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/axllent/mailpit/cmd.Version=${VERSION}" -o /mailpit CGO_ENABLED=0 go build -ldflags "-s -w -X github.com/axllent/mailpit/config.Version=${VERSION}" -o /mailpit
FROM alpine:latest FROM alpine:latest

View File

@ -5,21 +5,11 @@ import (
"os" "os"
"runtime" "runtime"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/updater" "github.com/axllent/mailpit/updater"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var (
// Version is the default application version, updated on release
Version = "dev"
// Repo on Github for updater
Repo = "axllent/mailpit"
// RepoBinaryName on Github for updater
RepoBinaryName = "mailpit"
)
// versionCmd represents the version command // versionCmd represents the version command
var versionCmd = &cobra.Command{ var versionCmd = &cobra.Command{
Use: "version", Use: "version",
@ -36,10 +26,10 @@ var versionCmd = &cobra.Command{
} }
fmt.Printf("%s %s compiled with %s on %s/%s\n", fmt.Printf("%s %s compiled with %s on %s/%s\n",
os.Args[0], Version, runtime.Version(), runtime.GOOS, runtime.GOARCH) os.Args[0], config.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
latest, _, _, err := updater.GithubLatest(Repo, RepoBinaryName) latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName)
if err == nil && updater.GreaterThan(latest, Version) { if err == nil && updater.GreaterThan(latest, config.Version) {
fmt.Printf( fmt.Printf(
"\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n", "\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n",
latest, latest,
@ -59,7 +49,7 @@ func init() {
} }
func updateApp() error { func updateApp() error {
rel, err := updater.GithubUpdate(Repo, RepoBinaryName, Version) rel, err := updater.GithubUpdate(config.Repo, config.RepoBinaryName, config.Version)
if err != nil { if err != nil {
return err return err
} }

View File

@ -58,6 +58,15 @@ var (
// ContentSecurityPolicy for HTTP server // ContentSecurityPolicy for HTTP server
ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';" ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"
// Version is the default application version, updated on release
Version = "dev"
// Repo on Github for updater
Repo = "axllent/mailpit"
// RepoBinaryName on Github for updater
RepoBinaryName = "mailpit"
) )
// VerifyConfig wil do some basic checking // VerifyConfig wil do some basic checking

View File

@ -22,15 +22,6 @@ type MessagesResult struct {
Messages []data.Summary `json:"messages"` Messages []data.Summary `json:"messages"`
} }
// // Mailbox returns an message overview (stats)
// func Mailbox(w http.ResponseWriter, _ *http.Request) {
// res := storage.StatsGet()
// bytes, _ := json.Marshal(res)
// w.Header().Add("Content-Type", "application/json")
// _, _ = w.Write(bytes)
// }
// Messages returns a paginated list of messages // Messages returns a paginated list of messages
func Messages(w http.ResponseWriter, r *http.Request) { func Messages(w http.ResponseWriter, r *http.Request) {
start, limit := getStartLimit(r) start, limit := getStartLimit(r)
@ -171,34 +162,6 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok")) _, _ = w.Write([]byte("ok"))
} }
// // DeleteMessage (method: DELETE) deletes a single message
// func DeleteMessage(w http.ResponseWriter, r *http.Request) {
// vars := mux.Vars(r)
// id := vars["id"]
// err := storage.DeleteOneMessage(id)
// if err != nil {
// httpError(w, err.Error())
// return
// }
// w.Header().Add("Content-Type", "text/plain")
// _, _ = w.Write([]byte("ok"))
// }
// SetAllRead (GET) will update all messages as read
// func SetAllRead(w http.ResponseWriter, r *http.Request) {
// err := storage.MarkAllRead()
// if err != nil {
// httpError(w, err.Error())
// return
// }
// w.Header().Add("Content-Type", "text/plain")
// _, _ = w.Write([]byte("ok"))
// }
// SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs // SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs
func SetReadStatus(w http.ResponseWriter, r *http.Request) { func SetReadStatus(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)

52
server/apiv1/info.go Normal file
View File

@ -0,0 +1,52 @@
package apiv1
import (
"encoding/json"
"net/http"
"os"
"runtime"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/updater"
)
type appVersion struct {
Version string
LatestVersion string
Database string
DatabaseSize int64
Messages int
Memory uint64
}
// AppInfo returns some basic details about the running app, and latest release.
func AppInfo(w http.ResponseWriter, r *http.Request) {
info := appVersion{}
info.Version = config.Version
latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName)
if err == nil {
info.LatestVersion = latest
}
info.Database = config.DataFile
db, err := os.Stat(info.Database)
if err == nil {
info.DatabaseSize = db.Size()
}
info.Messages = storage.CountTotal()
var m runtime.MemStats
runtime.ReadMemStats(&m)
info.Memory = m.Sys - m.HeapReleased
bytes, _ := json.Marshal(info)
w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(bytes)
}

View File

@ -66,6 +66,7 @@ func defaultRoutes() *mux.Router {
r.HandleFunc("/api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET") r.HandleFunc("/api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET") r.HandleFunc("/api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}", middleWareFunc(apiv1.Message)).Methods("GET") r.HandleFunc("/api/v1/message/{id}", middleWareFunc(apiv1.Message)).Methods("GET")
r.HandleFunc("/api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
return r return r
} }

View File

@ -28,7 +28,8 @@ export default {
notificationsSupported: false, notificationsSupported: false,
notificationsEnabled: false, notificationsEnabled: false,
selected: [], selected: [],
tcStatus: 0 tcStatus: 0,
appInfo : false,
} }
}, },
watch: { watch: {
@ -421,7 +422,7 @@ export default {
else if (Notification.permission !== "denied") { else if (Notification.permission !== "denied") {
let self = this; let self = this;
Notification.requestPermission().then(function (permission) { Notification.requestPermission().then(function (permission) {
// If the user accepts, let's create a notification // if the user accepts, let's create a notification
if (permission === "granted") { if (permission === "granted") {
self.browserNotify("Notifications enabled", "You will receive notifications when new mails are received."); self.browserNotify("Notifications enabled", "You will receive notifications when new mails are received.");
self.notificationsEnabled = true; self.notificationsEnabled = true;
@ -479,6 +480,14 @@ export default {
isSelected: function(id) { isSelected: function(id) {
return this.selected.indexOf(id) != -1; return this.selected.indexOf(id) != -1;
},
loadInfo: function() {
let self = this;
self.get('api/v1/info', false, function(response) {
self.appInfo = response.data;
self.modal('AppInfoModal').show();
});
} }
} }
} }
@ -625,13 +634,9 @@ export default {
</a> </a>
</li> </li>
<li class="mt-5 position-fixed bottom-0 bg-white py-2 text-muted"> <li class="mt-5 position-fixed bottom-0 bg-white py-2 text-muted">
<a href="https://github.com/axllent/mailpit" target="_blank" class="text-muted me-1"> <a href="#" class="text-muted" v-on:click="loadInfo">
<i class="bi bi-github"></i> <i class="bi bi-info-circle-fill"></i>
GitHub About
</a>
/
<a href="https://github.com/axllent/mailpit/wiki" target="_blank" class="text-muted ms-1">
Docs
</a> </a>
</li> </li>
</ul> </ul>
@ -756,4 +761,59 @@ export default {
</div> </div>
</div> </div>
<!-- Modal -->
<div class="modal fade" id="AppInfoModal" tabindex="-1" aria-labelledby="AppInfoModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header" v-if="appInfo">
<h5 class="modal-title" id="AppInfoModalLabel">
Mailpit
<code>({{ appInfo.Version }})</code>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<a class="btn btn-warning d-block mb-3" v-if="appInfo.Version != appInfo.LatestVersion"
:href="'https://github.com/axllent/mailpit/releases/tag/'+appInfo.LatestVersion">
A new version of Mailpit ({{ appInfo.LatestVersion }}) is available.
</a>
<div class="row g-3">
<div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit" target="_blank">
<i class="bi bi-github"></i>
Github
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki" target="_blank">
Documentation
<i class="bi bi-box-arrow-up-right"></i>
</a>
</div>
<div class="col-sm-6">
<div class="card border-secondary text-center">
<div class="card-header">Database size</div>
<div class="card-body text-secondary">
<h5 class="card-title">{{ getFileSize(appInfo.DatabaseSize) }} </h5>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card border-secondary text-center">
<div class="card-header">RAM usage</div>
<div class="card-body text-secondary">
<h5 class="card-title">{{ getFileSize(appInfo.Memory) }} </h5>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template> </template>

View File

@ -1,4 +1,6 @@
import axios from 'axios' import axios from 'axios';
import { Modal } from 'bootstrap';
// FakeModal is used to return a fake Bootstrap modal // FakeModal is used to return a fake Bootstrap modal
// if the ID returns nothing // if the ID returns nothing
@ -31,7 +33,7 @@ const commonMixins = {
// The request was made and the server responded with a status code // The request was made and the server responded with a status code
// that falls out of the range of 2xx // that falls out of the range of 2xx
if (error.response.data.Error) { if (error.response.data.Error) {
alert(error.response.data.Error) alert(error.response.data.Error);
} else { } else {
alert(error.response.data); alert(error.response.data);
} }
@ -50,7 +52,7 @@ const commonMixins = {
modal: function (id) { modal: function (id) {
let e = document.getElementById(id); let e = document.getElementById(id);
if (e) { if (e) {
return bootstrap.Modal.getOrCreateInstance(e); return Modal.getOrCreateInstance(e);
} }
// in case there are open/close actions // in case there are open/close actions
return new FakeModal(); return new FakeModal();

View File

@ -95,6 +95,8 @@ func InitDB() error {
p = filepath.Clean(p) p = filepath.Clean(p)
} }
config.DataFile = p
logger.Log().Debugf("[db] opening database %s", p) logger.Log().Debugf("[db] opening database %s", p)
var err error var err error