mirror of
				https://github.com/axllent/mailpit.git
				synced 2025-10-31 00:07:43 +02:00 
			
		
		
		
	UI: Add about app modal with version update notification
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/release-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 }}" | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										52
									
								
								server/apiv1/info.go
									
									
									
									
									
										Normal 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) | ||||||
|  | } | ||||||
| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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(); | ||||||
| @@ -209,4 +211,4 @@ const commonMixins = { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| export default commonMixins  | export default commonMixins | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user