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 }} | ||||
|         extra_files: LICENSE README.md | ||||
|         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 && \ | ||||
| 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 | ||||
|   | ||||
| @@ -5,21 +5,11 @@ import ( | ||||
| 	"os" | ||||
| 	"runtime" | ||||
|  | ||||
| 	"github.com/axllent/mailpit/config" | ||||
| 	"github.com/axllent/mailpit/updater" | ||||
| 	"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 | ||||
| var versionCmd = &cobra.Command{ | ||||
| 	Use:   "version", | ||||
| @@ -36,10 +26,10 @@ var versionCmd = &cobra.Command{ | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
| 		if err == nil && updater.GreaterThan(latest, Version) { | ||||
| 		latest, _, _, err := updater.GithubLatest(config.Repo, config.RepoBinaryName) | ||||
| 		if err == nil && updater.GreaterThan(latest, config.Version) { | ||||
| 			fmt.Printf( | ||||
| 				"\nUpdate available: %s\nRun `%s version -u` to update (requires read/write access to install directory).\n", | ||||
| 				latest, | ||||
| @@ -59,7 +49,7 @@ func init() { | ||||
| } | ||||
|  | ||||
| func updateApp() error { | ||||
| 	rel, err := updater.GithubUpdate(Repo, RepoBinaryName, Version) | ||||
| 	rel, err := updater.GithubUpdate(config.Repo, config.RepoBinaryName, config.Version) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|   | ||||
| @@ -58,6 +58,15 @@ var ( | ||||
|  | ||||
| 	// 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';" | ||||
|  | ||||
| 	// 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 | ||||
|   | ||||
| @@ -22,15 +22,6 @@ type MessagesResult struct { | ||||
| 	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 | ||||
| func Messages(w http.ResponseWriter, r *http.Request) { | ||||
| 	start, limit := getStartLimit(r) | ||||
| @@ -171,34 +162,6 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) { | ||||
| 	_, _ = 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 | ||||
| func SetReadStatus(w http.ResponseWriter, r *http.Request) { | ||||
| 	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}/thumb", middleWareFunc(apiv1.Thumbnail)).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 | ||||
| } | ||||
|   | ||||
| @@ -28,7 +28,8 @@ export default { | ||||
| 			notificationsSupported: false, | ||||
| 			notificationsEnabled: false, | ||||
| 			selected: [], | ||||
| 			tcStatus: 0 | ||||
| 			tcStatus: 0, | ||||
| 			appInfo : false, | ||||
| 		} | ||||
| 	}, | ||||
| 	watch: { | ||||
| @@ -421,7 +422,7 @@ export default { | ||||
| 			else if (Notification.permission !== "denied") { | ||||
| 				let self = this; | ||||
| 				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") { | ||||
| 						self.browserNotify("Notifications enabled", "You will receive notifications when new mails are received."); | ||||
| 						self.notificationsEnabled = true; | ||||
| @@ -479,6 +480,14 @@ export default { | ||||
|  | ||||
| 		isSelected: function(id) { | ||||
| 			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> | ||||
| 				</li> | ||||
| 				<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"> | ||||
| 						<i class="bi bi-github"></i> | ||||
| 						GitHub | ||||
| 					</a> | ||||
| 					/ | ||||
| 					<a href="https://github.com/axllent/mailpit/wiki" target="_blank" class="text-muted ms-1"> | ||||
| 						Docs | ||||
| 					<a href="#" class="text-muted" v-on:click="loadInfo"> | ||||
| 						<i class="bi bi-info-circle-fill"></i> | ||||
| 						About | ||||
| 					</a> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| @@ -756,4 +761,59 @@ export default { | ||||
| 		</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> | ||||
|   | ||||
| @@ -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 | ||||
| // if the ID returns nothing | ||||
| @@ -31,7 +33,7 @@ const commonMixins = { | ||||
| 				// The request was made and the server responded with a status code | ||||
| 				// that falls out of the range of 2xx | ||||
| 				if (error.response.data.Error) { | ||||
| 					alert(error.response.data.Error) | ||||
| 					alert(error.response.data.Error); | ||||
| 				} else { | ||||
| 					alert(error.response.data); | ||||
| 				} | ||||
| @@ -50,7 +52,7 @@ const commonMixins = { | ||||
| 		modal: function (id) { | ||||
| 			let e = document.getElementById(id); | ||||
| 			if (e) { | ||||
| 				return bootstrap.Modal.getOrCreateInstance(e); | ||||
| 				return Modal.getOrCreateInstance(e); | ||||
| 			} | ||||
| 			// in case there are open/close actions | ||||
| 			return new FakeModal(); | ||||
|   | ||||
| @@ -95,6 +95,8 @@ func InitDB() error { | ||||
| 		p = filepath.Clean(p) | ||||
| 	} | ||||
|  | ||||
| 	config.DataFile = p | ||||
|  | ||||
| 	logger.Log().Debugf("[db] opening database %s", p) | ||||
|  | ||||
| 	var err error | ||||
|   | ||||
		Reference in New Issue
	
	Block a user