mirror of
https://github.com/axllent/mailpit.git
synced 2025-01-04 00:15:54 +02:00
Merge branch 'release/0.1.3'
This commit is contained in:
commit
97bf9c257c
15
CHANGELOG.md
15
CHANGELOG.md
@ -3,6 +3,21 @@
|
|||||||
Notable changes to Mailpit will be documented in this file.
|
Notable changes to Mailpit will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Feature
|
||||||
|
- Mark all messages as read
|
||||||
|
|
||||||
|
### UI
|
||||||
|
- Better error handling when connection to server is broken
|
||||||
|
- Add reset search button
|
||||||
|
- Minor UI tweaks
|
||||||
|
- Update pagination values when new mail arrives when not on first page
|
||||||
|
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
- Merge pull request [#6](https://github.com/axllent/mailpit/issues/6) from KaptinLin/develop
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
### Feature
|
### Feature
|
||||||
|
12
cmd/root.go
12
cmd/root.go
@ -87,15 +87,21 @@ func init() {
|
|||||||
if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 {
|
if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 {
|
||||||
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
|
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
|
||||||
}
|
}
|
||||||
if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 {
|
|
||||||
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
|
|
||||||
}
|
|
||||||
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
|
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
|
||||||
config.UISSLCert = os.Getenv("MP_UI_SSL_CERT")
|
config.UISSLCert = os.Getenv("MP_UI_SSL_CERT")
|
||||||
}
|
}
|
||||||
if len(os.Getenv("MP_UI_SSL_KEY")) > 0 {
|
if len(os.Getenv("MP_UI_SSL_KEY")) > 0 {
|
||||||
config.UISSLKey = os.Getenv("MP_UI_SSL_KEY")
|
config.UISSLKey = os.Getenv("MP_UI_SSL_KEY")
|
||||||
}
|
}
|
||||||
|
if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 {
|
||||||
|
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
|
||||||
|
}
|
||||||
|
if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 {
|
||||||
|
config.SMTPSSLCert = os.Getenv("MP_SMTP_SSL_CERT")
|
||||||
|
}
|
||||||
|
if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 {
|
||||||
|
config.SMTPSSLKey = os.Getenv("MP_SMTP_SSL_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
// deprecated 2022/08/06
|
// deprecated 2022/08/06
|
||||||
if len(os.Getenv("MP_AUTH_FILE")) > 0 {
|
if len(os.Getenv("MP_AUTH_FILE")) > 0 {
|
||||||
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 42 KiB |
@ -218,6 +218,22 @@ func apiUnreadOne(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = w.Write([]byte("ok"))
|
_, _ = w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark single message as unread
|
||||||
|
func apiMarkAllRead(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
mailbox := vars["mailbox"]
|
||||||
|
|
||||||
|
err := storage.MarkAllRead(mailbox)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
|
||||||
// Websocket to broadcast changes
|
// Websocket to broadcast changes
|
||||||
func apiWebsocket(w http.ResponseWriter, r *http.Request) {
|
func apiWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
websockets.ServeWs(websockets.MessageHub, w, r)
|
websockets.ServeWs(websockets.MessageHub, w, r)
|
||||||
|
@ -39,6 +39,7 @@ func Listen() {
|
|||||||
r.HandleFunc("/api/{mailbox}/search", middleWareFunc(apiSearchMailbox))
|
r.HandleFunc("/api/{mailbox}/search", middleWareFunc(apiSearchMailbox))
|
||||||
r.HandleFunc("/api/{mailbox}/delete", middleWareFunc(apiDeleteAll))
|
r.HandleFunc("/api/{mailbox}/delete", middleWareFunc(apiDeleteAll))
|
||||||
r.HandleFunc("/api/{mailbox}/events", apiWebsocket)
|
r.HandleFunc("/api/{mailbox}/events", apiWebsocket)
|
||||||
|
r.HandleFunc("/api/{mailbox}/read", apiMarkAllRead)
|
||||||
r.HandleFunc("/api/{mailbox}/{id}/source", middleWareFunc(apiDownloadSource))
|
r.HandleFunc("/api/{mailbox}/{id}/source", middleWareFunc(apiDownloadSource))
|
||||||
r.HandleFunc("/api/{mailbox}/{id}/part/{partID}", middleWareFunc(apiDownloadAttachment))
|
r.HandleFunc("/api/{mailbox}/{id}/part/{partID}", middleWareFunc(apiDownloadAttachment))
|
||||||
r.HandleFunc("/api/{mailbox}/{id}/delete", middleWareFunc(apiDeleteOne))
|
r.HandleFunc("/api/{mailbox}/{id}/delete", middleWareFunc(apiDeleteOne))
|
||||||
|
@ -99,6 +99,13 @@ export default {
|
|||||||
this.loadMessages();
|
this.loadMessages();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetSearch: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.search = '';
|
||||||
|
this.scrollInPlace = true;
|
||||||
|
this.loadMessages();
|
||||||
|
},
|
||||||
|
|
||||||
reloadMessages: function() {
|
reloadMessages: function() {
|
||||||
this.search = "";
|
this.search = "";
|
||||||
this.start = 0;
|
this.start = 0;
|
||||||
@ -198,6 +205,16 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
markAllRead: function() {
|
||||||
|
let self = this;
|
||||||
|
let uri = 'api/' + self.mailbox + '/read'
|
||||||
|
self.get(uri, false, function(response) {
|
||||||
|
window.location.hash = "";
|
||||||
|
self.scrollInPlace = true;
|
||||||
|
self.loadMessages();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// websocket connect
|
// websocket connect
|
||||||
connect: function () {
|
connect: function () {
|
||||||
let wsproto = location.protocol == 'https:' ? 'wss' : 'ws';
|
let wsproto = location.protocol == 'https:' ? 'wss' : 'ws';
|
||||||
@ -210,12 +227,14 @@ export default {
|
|||||||
}
|
}
|
||||||
// new messages
|
// new messages
|
||||||
if (response.Type == "new" && response.Data) {
|
if (response.Type == "new" && response.Data) {
|
||||||
if (self.start < 1) {
|
if (!self.searching) {
|
||||||
if (!self.searching) {
|
if (self.start < 1) {
|
||||||
self.items.unshift(response.Data);
|
self.items.unshift(response.Data);
|
||||||
if (self.items.length > self.limit) {
|
if (self.items.length > self.limit) {
|
||||||
self.items.pop();
|
self.items.pop();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.start++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.total++;
|
self.total++;
|
||||||
@ -299,7 +318,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="navbar navbar-expand-lg navbar-light row flex-shrink-0 bg-light">
|
<div class="navbar navbar-expand-lg navbar-light row flex-shrink-0 bg-light shadow-sm">
|
||||||
<div class="col-lg-2 col-md-3 col-auto">
|
<div class="col-lg-2 col-md-3 col-auto">
|
||||||
<a class="navbar-brand" href="#" v-on:click="reloadMessages">
|
<a class="navbar-brand" href="#" v-on:click="reloadMessages">
|
||||||
<img src="mailpit.svg" alt="Mailpit">
|
<img src="mailpit.svg" alt="Mailpit">
|
||||||
@ -325,7 +344,10 @@ export default {
|
|||||||
<div class="col col-md-9 col-lg-5" v-if="!message && total">
|
<div class="col col-md-9 col-lg-5" v-if="!message && total">
|
||||||
<form v-on:submit="doSearch">
|
<form v-on:submit="doSearch">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" v-model.trim="search" placeholder="Search mailbox">
|
<div class="d-flex bg-white border rounded-start flex-fill position-relative">
|
||||||
|
<input type="text" class="form-control border-0" v-model.trim="search" placeholder="Search mailbox">
|
||||||
|
<span class="btn btn-link position-absolute end-0 text-muted" v-if="search" v-on:click="resetSearch"><i class="bi bi-x-circle"></i></span>
|
||||||
|
</div>
|
||||||
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
|
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -358,15 +380,15 @@ export default {
|
|||||||
<div class="row flex-fill" style="min-height:0">
|
<div class="row flex-fill" style="min-height:0">
|
||||||
<div class="d-none d-md-block col-lg-2 col-md-3 mh-100 position-relative" style="overflow-y: auto;">
|
<div class="d-none d-md-block col-lg-2 col-md-3 mh-100 position-relative" style="overflow-y: auto;">
|
||||||
<ul class="list-unstyled mt-3 mb-5">
|
<ul class="list-unstyled mt-3 mb-5">
|
||||||
<li v-if="isConnected" title="Messages will auto-load">
|
<li v-if="isConnected" title="Messages will auto-load" class="mb-2">
|
||||||
<i class="bi bi-power text-success"></i>
|
<i class="bi bi-power text-success"></i>
|
||||||
Connected
|
Connected
|
||||||
</li>
|
</li>
|
||||||
<li v-else title="Messages will auto-load">
|
<li v-else title="You need to manually refresh your mailbox" class="mb-3">
|
||||||
<i class="bi bi-power text-danger"></i>
|
<i class="bi bi-power text-danger"></i>
|
||||||
Disconnected
|
Disconnected
|
||||||
</li>
|
</li>
|
||||||
<li class="mt-3">
|
<li class="mb-5">
|
||||||
<a class="position-relative ps-0" href="#" v-on:click="reloadMessages">
|
<a class="position-relative ps-0" href="#" v-on:click="reloadMessages">
|
||||||
<i class="bi bi-envelope me-1" v-if="isConnected"></i>
|
<i class="bi bi-envelope me-1" v-if="isConnected"></i>
|
||||||
<i class="bi bi-arrow-clockwise me-1" v-else></i>
|
<i class="bi bi-arrow-clockwise me-1" v-else></i>
|
||||||
@ -376,20 +398,26 @@ export default {
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mt-3">
|
<li class="my-3" v-if="unread">
|
||||||
<a v-if="total" href="#" data-bs-toggle="modal" data-bs-target="#DeleteAllModal">
|
<a href="#" data-bs-toggle="modal" data-bs-target="#MarkAllReadModal">
|
||||||
|
<i class="bi bi-check2-square"></i>
|
||||||
|
Mark all read
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="my-3" v-if="total">
|
||||||
|
<a href="#" data-bs-toggle="modal" data-bs-target="#DeleteAllModal">
|
||||||
<i class="bi bi-trash-fill me-1 text-danger"></i>
|
<i class="bi bi-trash-fill me-1 text-danger"></i>
|
||||||
Delete all
|
Delete all
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mt-3" v-if="notificationsSupported && !notificationsEnabled">
|
<li class="my-3" v-if="notificationsSupported && !notificationsEnabled">
|
||||||
<a href="#" data-bs-toggle="modal" data-bs-target="#EnableNotificationsModal" title="Enable browser notifications">
|
<a href="#" data-bs-toggle="modal" data-bs-target="#EnableNotificationsModal" title="Enable browser notifications">
|
||||||
<i class="bi bi-bell"></i>
|
<i class="bi bi-bell"></i>
|
||||||
Enable alerts
|
Enable alerts
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mt-5 position-fixed bottom-0">
|
<li class="mt-5 position-fixed bottom-0">
|
||||||
<a href="https://github.com/axllent/mailpit" target="_blank" class="text-muted w-100 d-block bg-white py-2">
|
<a href="https://github.com/axllent/mailpit" target="_blank" class="text-muted w-100 d-block bg-white my-3">
|
||||||
<i class="bi bi-github"></i>
|
<i class="bi bi-github"></i>
|
||||||
GitHub
|
GitHub
|
||||||
</a>
|
</a>
|
||||||
@ -433,7 +461,14 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-muted py-3">No messages</div>
|
<div v-else class="text-muted my-3">
|
||||||
|
<span v-if="searching">
|
||||||
|
No results matching your search
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
There are no emails in your mailbox
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Message v-if="message" :message="message" :mailbox="mailbox"></Message>
|
<Message v-if="message" :message="message" :mailbox="mailbox"></Message>
|
||||||
@ -466,6 +501,25 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="MarkAllReadModal" tabindex="-1" aria-labelledby="MarkAllReadModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="MarkAllReadModalLabel">Mark all messages as read?</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
This will mark {{ formatNumber(unread) }} message<span v-if="unread > 1">s</span> as read.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" v-on:click="markAllRead">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div class="modal fade" id="EnableNotificationsModal" tabindex="-1" aria-labelledby="EnableNotificationsModalLabel" aria-hidden="true">
|
<div class="modal fade" id="EnableNotificationsModal" tabindex="-1" aria-labelledby="EnableNotificationsModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
// @import "../../../node_modules/bootstrap-icons"; ///scss/root";
|
|
||||||
|
|
||||||
@import "bootstrap";
|
@import "bootstrap";
|
||||||
|
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar {
|
||||||
color: #2d4a5d;
|
z-index: 99;
|
||||||
|
|
||||||
img {
|
.navbar-brand {
|
||||||
width: 40px;
|
color: #2d4a5d;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +27,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message.read:not(.active) {
|
.message.read:not(.active) {
|
||||||
// background: $gray-100;
|
|
||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,3 +53,7 @@
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group-item:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
@ -27,7 +27,7 @@ const commonMixins = {
|
|||||||
// Ajax error message
|
// Ajax error message
|
||||||
handleError: function (error) {
|
handleError: function (error) {
|
||||||
// handle error
|
// handle error
|
||||||
if (error.response) {
|
if (error.response && error.response.data) {
|
||||||
// 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) {
|
||||||
|
@ -574,3 +574,38 @@ func DeleteAllMessages(mailbox string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkAllRead will mark every unread message in a mailbox as read
|
||||||
|
func MarkAllRead(mailbox string) error {
|
||||||
|
mailbox = sanitizeMailboxName(mailbox)
|
||||||
|
|
||||||
|
totalStart := time.Now()
|
||||||
|
|
||||||
|
q, err := db.FindAll(clover.NewQuery(mailbox).
|
||||||
|
Where(clover.Field("Read").IsFalse()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(q)
|
||||||
|
|
||||||
|
updates := make(map[string]interface{})
|
||||||
|
updates["Read"] = true
|
||||||
|
|
||||||
|
for _, m := range q {
|
||||||
|
if err := db.UpdateById(mailbox, m.ObjectId(), updates); err != nil {
|
||||||
|
logger.Log().Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := statsRefresh(mailbox); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(totalStart)
|
||||||
|
|
||||||
|
logger.Log().Debugf("[db] marked %d messages in %s as read in %s", total, mailbox, elapsed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user