1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-05-15 22:16:44 +02:00

Chore: Refactor JavaScript, use arrow functions instead of "self" aliasing

This commit is contained in:
Ralph Slooten 2024-06-22 13:27:00 +12:00
parent 5e5b855a3d
commit 33e367d706
24 changed files with 357 additions and 396 deletions

View File

@ -26,15 +26,14 @@ export default {
}, },
methods: { methods: {
loadInfo: function () { loadInfo() {
let self = this this.get(this.resolve('/api/v1/info'), false, (response) => {
self.get(self.resolve('/api/v1/info'), false, function (response) {
mailbox.appInfo = response.data mailbox.appInfo = response.data
self.modal('AppInfoModal').show() this.modal('AppInfoModal').show()
}) })
}, },
requestNotifications: function () { requestNotifications() {
// check if the browser supports notifications // check if the browser supports notifications
if (!("Notification" in window)) { if (!("Notification" in window)) {
alert("This browser does not support desktop notifications") alert("This browser does not support desktop notifications")
@ -42,7 +41,6 @@ export default {
// we need to ask the user for permission // we need to ask the user for permission
else if (Notification.permission !== "denied") { else if (Notification.permission !== "denied") {
let self = this
Notification.requestPermission().then(function (permission) { Notification.requestPermission().then(function (permission) {
if (permission === "granted") { if (permission === "granted") {
mailbox.notificationsEnabled = true mailbox.notificationsEnabled = true
@ -180,7 +178,9 @@ export default {
<td> <td>
{{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPAccepted) }} {{ formatNumber(mailbox.appInfo.RuntimeStats.SMTPAccepted) }}
<small class="text-secondary"> <small class="text-secondary">
({{ getFileSize(mailbox.appInfo.RuntimeStats.SMTPAcceptedSize) }}) ({{
getFileSize(mailbox.appInfo.RuntimeStats.SMTPAcceptedSize)
}})
</small> </small>
</td> </td>
</tr> </tr>
@ -245,6 +245,6 @@ export default {
<Settings /> <Settings />
</template> </template>
<AjaxLoader :loading="loading" /> <AjaxLoader :loading="loading" />
</template> </template>

View File

@ -39,10 +39,9 @@ export default {
} }
this.iconProcessing = true this.iconProcessing = true
let self = this
window.setTimeout(() => { window.setTimeout(() => {
self.icoUpdate() this.icoUpdate()
}, this.iconTimeout) }, this.iconTimeout)
}, },
}, },

View File

@ -21,29 +21,25 @@ export default {
}, },
mounted() { mounted() {
let relativeTime = require('dayjs/plugin/relativeTime') const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
this.refreshUI() this.refreshUI()
}, },
methods: { methods: {
refreshUI: function () { refreshUI() {
let self = this window.setTimeout(() => {
window.setTimeout( this.$forceUpdate()
() => { this.refreshUI()
self.$forceUpdate() }, 30000)
self.refreshUI()
},
30000
)
}, },
getRelativeCreated: function (message) { getRelativeCreated(message) {
let d = new Date(message.Created) const d = new Date(message.Created)
return dayjs(d).fromNow() return dayjs(d).fromNow()
}, },
getPrimaryEmailTo: function (message) { getPrimaryEmailTo(message) {
for (let i in message.To) { for (let i in message.To) {
return message.To[i].Address return message.To[i].Address
} }
@ -51,11 +47,11 @@ export default {
return '[ Undisclosed recipients ]' return '[ Undisclosed recipients ]'
}, },
isSelected: function (id) { isSelected(id) {
return mailbox.selected.indexOf(id) != -1 return mailbox.selected.indexOf(id) != -1
}, },
toggleSelected: function (e, id) { toggleSelected(e, id) {
e.preventDefault() e.preventDefault()
if (this.isSelected(id)) { if (this.isSelected(id)) {
@ -67,7 +63,7 @@ export default {
} }
}, },
selectRange: function (e, id) { selectRange(e, id) {
e.preventDefault() e.preventDefault()
let selecting = false let selecting = false
@ -102,7 +98,7 @@ export default {
} }
}, },
toTagUrl: function (t) { toTagUrl(t) {
if (t.match(/ /)) { if (t.match(/ /)) {
t = `"${t}"` t = `"${t}"`
} }

View File

@ -30,7 +30,7 @@ export default {
}, },
methods: { methods: {
reloadInbox: function () { reloadInbox() {
const paginationParams = this.getPaginationParams() const paginationParams = this.getPaginationParams()
const reload = paginationParams?.start ? false : true const reload = paginationParams?.start ? false : true
@ -41,24 +41,22 @@ export default {
} }
}, },
loadMessages: function () { loadMessages() {
this.hideNav() // hide mobile menu this.hideNav() // hide mobile menu
this.$emit('loadMessages') this.$emit('loadMessages')
}, },
markAllRead: function () { markAllRead() {
let self = this this.put(this.resolve(`/api/v1/messages`), { 'read': true }, (response) => {
self.put(self.resolve(`/api/v1/messages`), { 'read': true }, function (response) {
window.scrollInPlace = true window.scrollInPlace = true
self.loadMessages() this.loadMessages()
}) })
}, },
deleteAllMessages: function () { deleteAllMessages() {
let self = this this.delete(this.resolve(`/api/v1/messages`), false, (response) => {
self.delete(self.resolve(`/api/v1/messages`), false, function (response) {
pagination.start = 0 pagination.start = 0
self.loadMessages() this.loadMessages()
}) })
} }
} }

View File

@ -30,22 +30,20 @@ export default {
}, },
methods: { methods: {
loadMessages: function () { loadMessages() {
this.hideNav() // hide mobile menu this.hideNav() // hide mobile menu
this.$emit('loadMessages') this.$emit('loadMessages')
}, },
deleteAllMessages: function () { deleteAllMessages() {
let s = this.getSearch() const s = this.getSearch()
if (!s) { if (!s) {
return return
} }
let self = this const uri = this.resolve(`/api/v1/search`) + '?query=' + encodeURIComponent(s)
this.delete(uri, false, (response) => {
let uri = this.resolve(`/api/v1/search`) + '?query=' + encodeURIComponent(s) this.$router.push('/')
this.delete(uri, false, function (response) {
self.$router.push('/')
}) })
} }
} }

View File

@ -19,54 +19,52 @@ export default {
}, },
methods: { methods: {
loadMessages: function () { loadMessages() {
this.$emit('loadMessages') this.$emit('loadMessages')
}, },
// mark selected messages as read // mark selected messages as read
markSelectedRead: function () { markSelectedRead() {
let self = this
if (!mailbox.selected.length) { if (!mailbox.selected.length) {
return false return false
} }
self.put(self.resolve(`/api/v1/messages`), { 'Read': true, 'IDs': mailbox.selected }, function (response) { this.put(this.resolve(`/api/v1/messages`), { 'Read': true, 'IDs': mailbox.selected }, (response) => {
window.scrollInPlace = true window.scrollInPlace = true
self.loadMessages() this.loadMessages()
}) })
}, },
isSelected: function (id) { isSelected(id) {
return mailbox.selected.indexOf(id) != -1 return mailbox.selected.indexOf(id) != -1
}, },
// mark selected messages as unread // mark selected messages as unread
markSelectedUnread: function () { markSelectedUnread() {
let self = this
if (!mailbox.selected.length) { if (!mailbox.selected.length) {
return false return false
} }
self.put(self.resolve(`/api/v1/messages`), { 'Read': false, 'IDs': mailbox.selected }, function (response) { this.put(this.resolve(`/api/v1/messages`), { 'Read': false, 'IDs': mailbox.selected }, (response) => {
window.scrollInPlace = true window.scrollInPlace = true
self.loadMessages() this.loadMessages()
}) })
}, },
// universal handler to delete current or selected messages // universal handler to delete current or selected messages
deleteMessages: function () { deleteMessages() {
let ids = [] let ids = []
let self = this
ids = JSON.parse(JSON.stringify(mailbox.selected)) ids = JSON.parse(JSON.stringify(mailbox.selected))
if (!ids.length) { if (!ids.length) {
return false return false
} }
self.delete(self.resolve(`/api/v1/messages`), { 'IDs': ids }, function (response) {
this.delete(this.resolve(`/api/v1/messages`), { 'IDs': ids }, (response) => {
window.scrollInPlace = true window.scrollInPlace = true
self.loadMessages() this.loadMessages()
}) })
}, },
// test if any selected emails are unread // test if any selected emails are unread
selectedHasUnread: function () { selectedHasUnread() {
if (!mailbox.selected.length) { if (!mailbox.selected.length) {
return false return false
} }
@ -79,7 +77,7 @@ export default {
}, },
// test of any selected emails are read // test of any selected emails are read
selectedHasRead: function () { selectedHasRead() {
if (!mailbox.selected.length) { if (!mailbox.selected.length) {
return false return false
} }

View File

@ -17,7 +17,7 @@ export default {
methods: { methods: {
// test whether a tag is currently being searched for (in the URL) // test whether a tag is currently being searched for (in the URL)
inSearch: function (tag) { inSearch(tag) {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const query = urlParams.get('q') const query = urlParams.get('q')
if (!query) { if (!query) {
@ -29,7 +29,7 @@ export default {
}, },
// toggle a tag search in the search URL, add or remove it accordingly // toggle a tag search in the search URL, add or remove it accordingly
toggleTag: function (e, tag) { toggleTag(e, tag) {
e.preventDefault() e.preventDefault()
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)

View File

@ -43,8 +43,7 @@ export default {
// websocket connect // websocket connect
connect() { connect() {
const ws = new WebSocket(this.socketURI) const ws = new WebSocket(this.socketURI)
let self = this ws.onmessage = (e) => {
ws.onmessage = function (e) {
let response let response
try { try {
response = JSON.parse(e.data) response = JSON.parse(e.data)
@ -65,7 +64,7 @@ export default {
// update pagination offset // update pagination offset
pagination.start++ pagination.start++
// prevent "Too many calls to Location or History APIs within a short timeframe" // prevent "Too many calls to Location or History APIs within a short timeframe"
self.delayedPaginationUpdate() this.delayedPaginationUpdate()
} }
} }
@ -79,13 +78,13 @@ export default {
} }
// send notifications // send notifications
if (!self.pauseNotifications) { if (!this.pauseNotifications) {
self.pauseNotifications = true this.pauseNotifications = true
let from = response.Data.From != null ? response.Data.From.Address : '[unknown]' let from = response.Data.From != null ? response.Data.From.Address : '[unknown]'
self.browserNotify("New mail from: " + from, response.Data.Subject) this.browserNotify("New mail from: " + from, response.Data.Subject)
self.setMessageToast(response.Data) this.setMessageToast(response.Data)
// delay notifications by 2s // delay notifications by 2s
window.setTimeout(() => { self.pauseNotifications = false }, 2000) window.setTimeout(() => { this.pauseNotifications = false }, 2000)
} }
} else if (response.Type == "prune") { } else if (response.Type == "prune") {
// messages have been deleted, reload messages to adjust // messages have been deleted, reload messages to adjust
@ -98,24 +97,24 @@ export default {
mailbox.unread = response.Data.Unread mailbox.unread = response.Data.Unread
// detect version updated, refresh is needed // detect version updated, refresh is needed
if (self.version != response.Data.Version) { if (this.version != response.Data.Version) {
location.reload() location.reload()
} }
} }
} }
ws.onopen = function () { ws.onopen = () => {
mailbox.connected = true mailbox.connected = true
self.socketLastConnection = Date.now() this.socketLastConnection = Date.now()
if (self.reconnectRefresh) { if (this.reconnectRefresh) {
self.reconnectRefresh = false this.reconnectRefresh = false
mailbox.refresh = true // trigger refresh mailbox.refresh = true // trigger refresh
window.setTimeout(() => { mailbox.refresh = false }, 500) window.setTimeout(() => { mailbox.refresh = false }, 500)
} }
} }
ws.onclose = function (e) { ws.onclose = (e) => {
if (self.socketLastConnection == 0) { if (this.socketLastConnection == 0) {
// connection failed immediately after connecting to Mailpit implies proxy websockets aren't configured // connection failed immediately after connecting to Mailpit implies proxy websockets aren't configured
console.log('Unable to connect to websocket, disabling websocket support') console.log('Unable to connect to websocket, disabling websocket support')
return return
@ -123,27 +122,27 @@ export default {
if (mailbox.connected) { if (mailbox.connected) {
// count disconnections // count disconnections
self.socketBreaks++ this.socketBreaks++
} }
// set disconnected state // set disconnected state
mailbox.connected = false mailbox.connected = false
if (self.socketBreaks > 3) { if (this.socketBreaks > 3) {
// give up after > 3 successful socket connections & disconnections within a 15 second window, // give up after > 3 successful socket connections & disconnections within a 15 second window,
// something is not working right on their end, see issue #319 // something is not working right on their end, see issue #319
console.log('Unstable websocket connection, disabling websocket support') console.log('Unstable websocket connection, disabling websocket support')
return return
} }
if (Date.now() - self.socketLastConnection > 5000) { if (Date.now() - this.socketLastConnection > 5000) {
// only refresh mailbox if the last successful connection was broken for > 5 seconds // only refresh mailbox if the last successful connection was broken for > 5 seconds
self.reconnectRefresh = true this.reconnectRefresh = true
} else { } else {
self.reconnectRefresh = false this.reconnectRefresh = false
} }
setTimeout(function () { setTimeout(() => {
self.connect() // reconnect this.connect() // reconnect
}, 1000) }, 1000)
} }
@ -214,11 +213,10 @@ export default {
this.toastMessage = m this.toastMessage = m
let self = this
const el = document.getElementById('messageToast') const el = document.getElementById('messageToast')
if (el) { if (el) {
el.addEventListener('hidden.bs.toast', () => { el.addEventListener('hidden.bs.toast', () => {
self.toastMessage = false this.toastMessage = false
}) })
Toast.getOrCreateInstance(el).show() Toast.getOrCreateInstance(el).show()

View File

@ -20,16 +20,16 @@ export default {
}, },
computed: { computed: {
canPrev: function () { canPrev() {
return pagination.start > 0 return pagination.start > 0
}, },
canNext: function () { canNext() {
return this.total > (pagination.start + mailbox.messages.length) return this.total > (pagination.start + mailbox.messages.length)
}, },
// returns the number of next X messages // returns the number of next X messages
nextMessages: function () { nextMessages() {
let t = pagination.start + parseInt(pagination.limit, 10) let t = pagination.start + parseInt(pagination.limit, 10)
if (t > this.total) { if (t > this.total) {
t = this.total t = this.total
@ -40,17 +40,17 @@ export default {
}, },
methods: { methods: {
changeLimit: function () { changeLimit() {
pagination.start = 0 pagination.start = 0
this.updateQueryParams() this.updateQueryParams()
}, },
viewNext: function () { viewNext() {
pagination.start = parseInt(pagination.start, 10) + parseInt(pagination.limit, 10) pagination.start = parseInt(pagination.start, 10) + parseInt(pagination.limit, 10)
this.updateQueryParams() this.updateQueryParams()
}, },
viewPrev: function () { viewPrev() {
let s = pagination.start - pagination.limit let s = pagination.start - pagination.limit
if (s < 0) { if (s < 0) {
s = 0 s = 0
@ -59,7 +59,7 @@ export default {
this.updateQueryParams() this.updateQueryParams()
}, },
updateQueryParams: function () { updateQueryParams() {
const path = this.$route.path const path = this.$route.path
const p = { const p = {
...this.$route.query ...this.$route.query

View File

@ -24,18 +24,18 @@ export default {
}, },
methods: { methods: {
searchFromURL: function () { searchFromURL() {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
this.search = urlParams.get('q') ? urlParams.get('q') : '' this.search = urlParams.get('q') ? urlParams.get('q') : ''
}, },
doSearch: function (e) { doSearch(e) {
pagination.start = 0 pagination.start = 0
if (this.search == '') { if (this.search == '') {
this.$router.push('/') this.$router.push('/')
} else { } else {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
let curr = urlParams.get('q') const curr = urlParams.get('q')
if (curr && curr == this.search) { if (curr && curr == this.search) {
pagination.start = 0 pagination.start = 0
this.$emit('loadMessages') this.$emit('loadMessages')
@ -57,7 +57,7 @@ export default {
e.preventDefault() e.preventDefault()
}, },
resetSearch: function () { resetSearch() {
this.search = '' this.search = ''
this.$router.push('/') this.$router.push('/')
} }

View File

@ -16,7 +16,7 @@ export default {
}, },
watch: { watch: {
theme: function (v) { theme(v) {
if (v == 'auto') { if (v == 'auto') {
localStorage.removeItem('theme') localStorage.removeItem('theme')
} else { } else {
@ -34,7 +34,7 @@ export default {
}, },
methods: { methods: {
setTheme: function () { setTheme() {
if ( if (
this.theme === 'auto' && this.theme === 'auto' &&
window.matchMedia('(prefers-color-scheme: dark)').matches window.matchMedia('(prefers-color-scheme: dark)').matches

View File

@ -18,7 +18,7 @@ export default {
}, },
methods: { methods: {
openAttachment: function (part, e) { openAttachment(part, e) {
let filename = part.FileName let filename = part.FileName
let contentType = part.ContentType let contentType = part.ContentType
let href = this.resolve('/api/v1/message/' + this.message.ID + '/part/' + part.PartID) let href = this.resolve('/api/v1/message/' + this.message.ID + '/part/' + part.PartID)

View File

@ -41,9 +41,7 @@ export default {
}, },
computed: { computed: {
summary: function () { summary() {
let self = this
if (!this.check) { if (!this.check) {
return false return false
} }
@ -65,8 +63,8 @@ export default {
} }
// filter by enabled platforms // filter by enabled platforms
let results = o.Results.filter(function (w) { let results = o.Results.filter((w) => {
return self.platforms.indexOf(w.Platform) != -1 return this.platforms.indexOf(w.Platform) != -1
}) })
if (results.length == 0) { if (results.length == 0) {
@ -98,7 +96,7 @@ export default {
} }
let maxPartial = 0, maxUnsupported = 0 let maxPartial = 0, maxUnsupported = 0
result.Warnings.forEach(function (w) { result.Warnings.forEach((w) => {
let scoreWeight = 1 let scoreWeight = 1
if (w.Score.Found < result.Total.Nodes) { if (w.Score.Found < result.Total.Nodes) {
// each error is weighted based on the number of occurrences vs: the total message nodes // each error is weighted based on the number of occurrences vs: the total message nodes
@ -108,7 +106,7 @@ export default {
// pseudo-classes & at-rules need to be weighted lower as we do not know how many times they // pseudo-classes & at-rules need to be weighted lower as we do not know how many times they
// are actually used in the HTML, and including things like bootstrap styles completely throws // are actually used in the HTML, and including things like bootstrap styles completely throws
// off the calculation as these dominate. // off the calculation as these dominate.
if (self.isPseudoClassOrAtRule(w.Title)) { if (this.isPseudoClassOrAtRule(w.Title)) {
scoreWeight = 0.05 scoreWeight = 0.05
w.PseudoClassOrAtRule = true w.PseudoClassOrAtRule = true
} }
@ -124,15 +122,15 @@ export default {
}) })
// sort warnings by final score // sort warnings by final score
result.Warnings.sort(function (a, b) { result.Warnings.sort((a, b) => {
let aWeight = a.Score.Found > result.Total.Nodes ? result.Total.Nodes : a.Score.Found / result.Total.Nodes let aWeight = a.Score.Found > result.Total.Nodes ? result.Total.Nodes : a.Score.Found / result.Total.Nodes
let bWeight = b.Score.Found > result.Total.Nodes ? result.Total.Nodes : b.Score.Found / result.Total.Nodes let bWeight = b.Score.Found > result.Total.Nodes ? result.Total.Nodes : b.Score.Found / result.Total.Nodes
if (self.isPseudoClassOrAtRule(a.Title)) { if (this.isPseudoClassOrAtRule(a.Title)) {
aWeight = 0.05 aWeight = 0.05
} }
if (self.isPseudoClassOrAtRule(b.Title)) { if (this.isPseudoClassOrAtRule(b.Title)) {
bWeight = 0.05 bWeight = 0.05
} }
@ -148,7 +146,7 @@ export default {
return result return result
}, },
graphSections: function () { graphSections() {
let s = Math.round(this.summary.Total.Supported) let s = Math.round(this.summary.Total.Supported)
let p = Math.round(this.summary.Total.Partial) let p = Math.round(this.summary.Total.Partial)
let u = 100 - s - p let u = 100 - s - p
@ -172,7 +170,7 @@ export default {
}, },
// colors depend on both varying unsupported & partially unsupported percentages // colors depend on both varying unsupported & partially unsupported percentages
scoreColor: function () { scoreColor() {
if (this.summary.Total.Unsupported < 5 && this.summary.Total.Partial < 10) { if (this.summary.Total.Unsupported < 5 && this.summary.Total.Partial < 10) {
this.$emit('setBadgeStyle', 'bg-success') this.$emit('setBadgeStyle', 'bg-success')
return 'text-success' return 'text-success'
@ -197,62 +195,51 @@ export default {
platforms(v) { platforms(v) {
localStorage.setItem('html-check-platforms', JSON.stringify(v)) localStorage.setItem('html-check-platforms', JSON.stringify(v))
}, },
// enabled(v) {
// if (!v) {
// localStorage.setItem('htmlCheckDisabled', true)
// this.$emit('setHtmlScore', false)
// } else {
// localStorage.removeItem('htmlCheckDisabled')
// this.doCheck()
// }
// }
}, },
methods: { methods: {
doCheck: function () { doCheck() {
this.check = false this.check = false
if (this.message.HTML == "") { if (this.message.HTML == "") {
return return
} }
let self = this
// ignore any error, do not show loader // ignore any error, do not show loader
axios.get(self.resolve('/api/v1/message/' + self.message.ID + '/html-check'), null) axios.get(this.resolve('/api/v1/message/' + this.message.ID + '/html-check'), null)
.then(function (result) { .then((result) => {
self.check = result.data this.check = result.data
self.error = false this.error = false
// set tooltips // set tooltips
window.setTimeout(function () { window.setTimeout(() => {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl)) [...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl))
}, 500) }, 500)
}) })
.catch(function (error) { .catch((error) => {
// handle error // handle error
if (error.response && error.response.data) { 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) {
self.error = error.response.data.Error this.error = error.response.data.Error
} else { } else {
self.error = error.response.data this.error = error.response.data
} }
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
self.error = 'Error sending data to the server. Please try again.' this.error = 'Error sending data to the server. Please try again.'
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
self.error = error.message this.error = error.message
} }
}) })
}, },
loadConfig: function () { loadConfig() {
let platforms = localStorage.getItem('html-check-platforms') let platforms = localStorage.getItem('html-check-platforms')
if (platforms) { if (platforms) {
try { try {
@ -268,7 +255,7 @@ export default {
}, },
// return a platform's families (email clients) // return a platform's families (email clients)
families: function (k) { families(k) {
if (this.check.Platforms[k]) { if (this.check.Platforms[k]) {
return this.check.Platforms[k] return this.check.Platforms[k]
} }
@ -277,19 +264,19 @@ export default {
}, },
// return whether the test string is a pseudo class (:<test>) or at rule (@<test>) // return whether the test string is a pseudo class (:<test>) or at rule (@<test>)
isPseudoClassOrAtRule: function (t) { isPseudoClassOrAtRule(t) {
return t.match(/^(:|@)/) return t.match(/^(:|@)/)
}, },
round: function (v) { round(v) {
return Math.round(v) return Math.round(v)
}, },
round2dm: function (v) { round2dm(v) {
return Math.round(v * 100) / 100 return Math.round(v * 100) / 100
}, },
scrollToWarnings: function () { scrollToWarnings() {
if (!this.$refs.warnings) { if (!this.$refs.warnings) {
return return
} }
@ -312,9 +299,9 @@ export default {
<div class="mt-5 mb-3"> <div class="mt-5 mb-3">
<div class="row w-100"> <div class="row w-100">
<div class="col-md-8"> <div class="col-md-8">
<Donut :sections="graphSections" background="var(--bs-body-bg)" :size="180" unit="px" :thickness="20" <Donut :sections="graphSections" background="var(--bs-body-bg)" :size="180" unit="px"
has-legend legend-placement="bottom" :total="100" :start-angle="0" :auto-adjust-text-size="true" :thickness="20" has-legend legend-placement="bottom" :total="100" :start-angle="0"
@section-click="scrollToWarnings"> :auto-adjust-text-size="true" @section-click="scrollToWarnings">
<h2 class="m-0" :class="scoreColor" @click="scrollToWarnings"> <h2 class="m-0" :class="scoreColor" @click="scrollToWarnings">
{{ round2dm(summary.Total.Supported) }}% {{ round2dm(summary.Total.Supported) }}%
</h2> </h2>
@ -388,8 +375,9 @@ export default {
<div class="col-sm mt-2 mt-sm-0"> <div class="col-sm mt-2 mt-sm-0">
<div class="progress-stacked"> <div class="progress-stacked">
<div class="progress" role="progressbar" aria-label="Supported" <div class="progress" role="progressbar" aria-label="Supported"
:aria-valuenow="warning.Score.Supported" aria-valuemin="0" aria-valuemax="100" :aria-valuenow="warning.Score.Supported" aria-valuemin="0"
:style="{ width: warning.Score.Supported + '%' }" title="Supported"> aria-valuemax="100" :style="{ width: warning.Score.Supported + '%' }"
title="Supported">
<div class="progress-bar bg-success"> <div class="progress-bar bg-success">
{{ round(warning.Score.Supported) + '%' }} {{ round(warning.Score.Supported) + '%' }}
</div> </div>
@ -402,8 +390,9 @@ export default {
</div> </div>
</div> </div>
<div class="progress" role="progressbar" aria-label="No" <div class="progress" role="progressbar" aria-label="No"
:aria-valuenow="warning.Score.Unsupported" aria-valuemin="0" aria-valuemax="100" :aria-valuenow="warning.Score.Unsupported" aria-valuemin="0"
:style="{ width: warning.Score.Unsupported + '%' }" title="Not supported"> aria-valuemax="100" :style="{ width: warning.Score.Unsupported + '%' }"
title="Not supported">
<div class="progress-bar bg-danger"> <div class="progress-bar bg-danger">
{{ round(warning.Score.Unsupported) + '%' }} {{ round(warning.Score.Unsupported) + '%' }}
</div> </div>
@ -420,7 +409,8 @@ export default {
<i class="bi bi-info-circle me-2"></i> <i class="bi bi-info-circle me-2"></i>
Detected {{ warning.Score.Found }} <code>{{ warning.Title }}</code> Detected {{ warning.Score.Found }} <code>{{ warning.Title }}</code>
propert<template v-if="warning.Score.Found === 1">y</template><template propert<template v-if="warning.Score.Found === 1">y</template><template
v-else>ies</template> in the CSS styles, but unable to test if used or not. v-else>ies</template> in the CSS
styles, but unable to test if used or not.
</span> </span>
<span v-if="warning.Description != ''" v-html="warning.Description" class="me-2"></span> <span v-if="warning.Description != ''" v-html="warning.Description" class="me-2"></span>
</p> </p>
@ -487,9 +477,9 @@ export default {
<div id="col1" class="accordion-collapse collapse" <div id="col1" class="accordion-collapse collapse"
data-bs-parent="#HTMLCheckAboutAccordion"> data-bs-parent="#HTMLCheckAboutAccordion">
<div class="accordion-body"> <div class="accordion-body">
The support for HTML/CSS messages varies greatly across email clients. HTML check The support for HTML/CSS messages varies greatly across email clients. HTML
attempts to calculate the overall support for your email for all selected platforms check attempts to calculate the overall support for your email for all selected
to give you some idea of the general compatibility of your HTML email. platforms to give you some idea of the general compatibility of your HTML email.
</div> </div>
</div> </div>
</div> </div>
@ -507,29 +497,31 @@ export default {
Internally the original HTML message is run against Internally the original HTML message is run against
<b>{{ check.Total.Tests }}</b> different HTML and CSS tests. All tests <b>{{ check.Total.Tests }}</b> different HTML and CSS tests. All tests
(except for <code>&lt;script&gt;</code>) correspond to a test on (except for <code>&lt;script&gt;</code>) correspond to a test on
<a href="https://www.caniemail.com/" target="_blank">caniemail.com</a>, and the <a href="https://www.caniemail.com/" target="_blank">caniemail.com</a>, and
final score is calculated using the available compatibility data. the final score is calculated using the available compatibility data.
</p> </p>
<p> <p>
CSS support is very difficult to programmatically test, especially if a message CSS support is very difficult to programmatically test, especially if a
contains CSS style blocks or is linked to remote stylesheets. Remote stylesheets message contains CSS style blocks or is linked to remote stylesheets. Remote
are, unless blocked via <code>--block-remote-css-and-fonts</code>, downloaded stylesheets are, unless blocked via
and injected into the message as style blocks. The email is then <code>--block-remote-css-and-fonts</code>,
<a href="https://github.com/vanng822/go-premailer" target="_blank">inlined</a> downloaded and injected into the message as style blocks. The email is then
<a href="https://github.com/vanng822/go-premailer"
target="_blank">inlined</a>
to matching HTML elements. This gives Mailpit fairly accurate results. to matching HTML elements. This gives Mailpit fairly accurate results.
</p> </p>
<p> <p>
CSS properties such as <code>@font-face</code>, <code>:visited</code>, CSS properties such as <code>@font-face</code>, <code>:visited</code>,
<code>:hover</code> etc cannot be inlined however, so these are searched for <code>:hover</code> etc cannot be inlined however, so these are searched for
within CSS blocks. This method is not accurate as Mailpit does not know how many within CSS blocks. This method is not accurate as Mailpit does not know how
nodes it actually applies to, if any, so they are weighted lightly (5%) as not many nodes it actually applies to, if any, so they are weighted lightly (5%)
to affect the score. An example of this would be any email linking to the full as not to affect the score. An example of this would be any email linking to
bootstrap CSS which contains dozens of unused attributes. the full bootstrap CSS which contains dozens of unused attributes.
</p> </p>
<p> <p>
All warnings are displayed with their respective support, including any specific All warnings are displayed with their respective support, including any
notes, and it is up to you to decide what you do with that information and how specific notes, and it is up to you to decide what you do with that
badly it may impact your message. information and how badly it may impact your message.
</p> </p>
</div> </div>
</div> </div>
@ -552,13 +544,15 @@ export default {
<p> <p>
For each test, Mailpit calculates both the unsupported & partially-supported For each test, Mailpit calculates both the unsupported & partially-supported
percentages in relation to the number of matches against the total number of percentages in relation to the number of matches against the total number of
nodes (elements) in the HTML. The maximum unsupported and partially-supported nodes (elements) in the HTML. The maximum unsupported and
weighted scores are then used for the final score (ie: worst case scenario). partially-supported weighted scores are then used for the final score (ie:
worst case scenario).
</p> </p>
<p> <p>
To try explain this logic in very simple terms: Assuming a To try explain this logic in very simple terms: Assuming a
<code>&lt;script&gt;</code> node (element) has 100% failure (not supported in <code>&lt;script&gt;</code> node (element) has 100% failure (not supported
any email client), and a <code>&lt;p&gt;</code> node has 100% pass (supported). in any email client), and a <code>&lt;p&gt;</code> node has 100% pass
(supported).
</p> </p>
<ul> <ul>
<li> <li>
@ -575,7 +569,8 @@ export default {
</li> </li>
</ul> </ul>
<p> <p>
Mailpit will sort the warnings according to their weighted unsupported scores. Mailpit will sort the warnings according to their weighted unsupported
scores.
</p> </p>
</div> </div>
</div> </div>
@ -591,9 +586,9 @@ export default {
<div id="col4" class="accordion-collapse collapse" <div id="col4" class="accordion-collapse collapse"
data-bs-parent="#HTMLCheckAboutAccordion"> data-bs-parent="#HTMLCheckAboutAccordion">
<div class="accordion-body"> <div class="accordion-body">
HTML check does not detect if the original HTML is valid. In order to detect applied HTML check does not detect if the original HTML is valid. In order to detect
styles to every node, the HTML email is run through a parser which is very good at applied styles to every node, the HTML email is run through a parser which is
turning invalid input into valid output. It is what it is... very good at turning invalid input into valid output. It is what it is...
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,3 @@
<script> <script>
import commonMixins from '../../mixins/CommonMixins' import commonMixins from '../../mixins/CommonMixins'
@ -16,10 +15,9 @@ export default {
}, },
mounted() { mounted() {
let self = this; let uri = this.resolve('/api/v1/message/' + this.message.ID + '/headers')
let uri = self.resolve('/api/v1/message/' + self.message.ID + '/headers') this.get(uri, false, (response) => {
self.get(uri, false, function (response) { this.headers = response.data
self.headers = response.data
}); });
}, },

View File

@ -64,7 +64,7 @@ export default {
}, },
computed: { computed: {
groupedStatuses: function () { groupedStatuses() {
let results = {} let results = {}
if (!this.check) { if (!this.check) {
@ -114,7 +114,7 @@ export default {
}, },
methods: { methods: {
doCheck: function () { doCheck() {
this.check = false this.check = false
this.loading = true this.loading = true
let uri = this.resolve('/api/v1/message/' + this.message.ID + '/link-check') let uri = this.resolve('/api/v1/message/' + this.message.ID + '/link-check')
@ -122,38 +122,37 @@ export default {
uri += '?follow=true' uri += '?follow=true'
} }
let self = this
// ignore any error, do not show loader // ignore any error, do not show loader
axios.get(uri, null) axios.get(uri, null)
.then(function (result) { .then((result) => {
self.check = result.data this.check = result.data
self.error = false this.error = false
self.$emit('setLinkErrors', result.data.Errors) this.$emit('setLinkErrors', result.data.Errors)
}) })
.catch(function (error) { .catch((error) => {
// handle error // handle error
if (error.response && error.response.data) { 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) {
self.error = error.response.data.Error this.error = error.response.data.Error
} else { } else {
self.error = error.response.data this.error = error.response.data
} }
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
self.error = 'Error sending data to the server. Please try again.' this.error = 'Error sending data to the server. Please try again.'
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
self.error = error.message this.error = error.message
} }
}) })
.then(function (result) { .then((result) => {
// always run // always run
self.loading = false this.loading = false
}) })
}, },
} }
@ -239,7 +238,8 @@ export default {
</div> </div>
<div class="modal fade" id="LinkCheckOptions" tabindex="-1" aria-labelledby="LinkCheckOptionsLabel" aria-hidden="true"> <div class="modal fade" id="LinkCheckOptions" tabindex="-1" aria-labelledby="LinkCheckOptionsLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -296,11 +296,12 @@ export default {
What is Link check? What is Link check?
</button> </button>
</h2> </h2>
<div id="col1" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion"> <div id="col1" class="accordion-collapse collapse"
data-bs-parent="#LinkCheckAboutAccordion">
<div class="accordion-body"> <div class="accordion-body">
Link check scans your message HTML and text for all unique links, images and linked Link check scans your message HTML and text for all unique links, images and linked
stylesheets. It then does a HTTP <code>HEAD</code> request to each link, 5 at a time, to stylesheets. It then does a HTTP <code>HEAD</code> request to each link, 5 at a
test whether the link/image/stylesheet exists. time, to test whether the link/image/stylesheet exists.
</div> </div>
</div> </div>
</div> </div>
@ -311,14 +312,16 @@ export default {
What are "301" and "302" links? What are "301" and "302" links?
</button> </button>
</h2> </h2>
<div id="col2" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion"> <div id="col2" class="accordion-collapse collapse"
data-bs-parent="#LinkCheckAboutAccordion">
<div class="accordion-body"> <div class="accordion-body">
<p> <p>
These are links that redirect you to another URL, for example newsletters These are links that redirect you to another URL, for example newsletters
often use redirect links to track user clicks. often use redirect links to track user clicks.
</p> </p>
<p> <p>
By default Link check will not follow these links, however you can turn this on via By default Link check will not follow these links, however you can turn this on
via
the settings and Link check will "follow" those redirects. the settings and Link check will "follow" those redirects.
</p> </p>
</div> </div>
@ -331,7 +334,8 @@ export default {
Why are some links returning an error but work in my browser? Why are some links returning an error but work in my browser?
</button> </button>
</h2> </h2>
<div id="col3" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion"> <div id="col3" class="accordion-collapse collapse"
data-bs-parent="#LinkCheckAboutAccordion">
<div class="accordion-body"> <div class="accordion-body">
<p>This may be due to various reasons, for instance:</p> <p>This may be due to various reasons, for instance:</p>
<ul> <ul>
@ -353,11 +357,12 @@ export default {
What are the risks of running Link check automatically? What are the risks of running Link check automatically?
</button> </button>
</h2> </h2>
<div id="col4" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion"> <div id="col4" class="accordion-collapse collapse"
data-bs-parent="#LinkCheckAboutAccordion">
<div class="accordion-body"> <div class="accordion-body">
<p> <p>
Depending on the type of messages you are testing, opening all links on all messages Depending on the type of messages you are testing, opening all links on all
may have undesired consequences: messages may have undesired consequences:
</p> </p>
<ul> <ul>
<li>If the message contains tracking links this may reveal your identity.</li> <li>If the message contains tracking links this may reveal your identity.</li>
@ -366,13 +371,13 @@ export default {
unsubscribe you. unsubscribe you.
</li> </li>
<li> <li>
To speed up the checking process, Link check will attempt 5 URLs at a time. This To speed up the checking process, Link check will attempt 5 URLs at a time.
could lead to temporary heady load on the remote server. This could lead to temporary heady load on the remote server.
</li> </li>
</ul> </ul>
<p> <p>
Unless you know what messages you receive, it is advised to only run the Link check Unless you know what messages you receive, it is advised to only run the Link
manually. check manually.
</p> </p>
</div> </div>
</div> </div>

View File

@ -61,16 +61,15 @@ export default {
scaleHTMLPreview(v) { scaleHTMLPreview(v) {
if (v == 'display') { if (v == 'display') {
let self = this window.setTimeout(() => {
window.setTimeout(function () { this.resizeIFrames()
self.resizeIFrames()
}, 500) }, 500)
} }
} }
}, },
computed: { computed: {
hasAnyChecksEnabled: function () { hasAnyChecksEnabled() {
return (mailbox.showHTMLCheck && this.message.HTML) return (mailbox.showHTMLCheck && this.message.HTML)
|| mailbox.showLinkCheck || mailbox.showLinkCheck
|| (mailbox.showSpamCheck && mailbox.uiConfig.SpamAssassin) || (mailbox.showSpamCheck && mailbox.uiConfig.SpamAssassin)
@ -78,56 +77,53 @@ export default {
}, },
mounted() { mounted() {
let self = this this.canSaveTags = false
self.canSaveTags = false this.messageTags = this.message.Tags
self.messageTags = self.message.Tags this.renderUI()
self.renderUI()
window.addEventListener("resize", self.resizeIFrames) window.addEventListener("resize", this.resizeIFrames)
let headersTab = document.getElementById('nav-headers-tab') let headersTab = document.getElementById('nav-headers-tab')
headersTab.addEventListener('shown.bs.tab', function (event) { headersTab.addEventListener('shown.bs.tab', (event) => {
self.loadHeaders = true this.loadHeaders = true
}) })
let rawTab = document.getElementById('nav-raw-tab') let rawTab = document.getElementById('nav-raw-tab')
rawTab.addEventListener('shown.bs.tab', function (event) { rawTab.addEventListener('shown.bs.tab', (event) => {
self.srcURI = self.resolve('/api/v1/message/' + self.message.ID + '/raw') this.srcURI = this.resolve('/api/v1/message/' + this.message.ID + '/raw')
self.resizeIFrames() this.resizeIFrames()
}) })
// manually refresh tags // manually refresh tags
self.get(self.resolve(`/api/v1/tags`), false, function (response) { this.get(this.resolve(`/api/v1/tags`), false, (response) => {
self.availableTags = response.data this.availableTags = response.data
self.$nextTick(function () { this.$nextTick(() => {
Tags.init('select[multiple]') Tags.init('select[multiple]')
// delay tag change detection to allow Tags to load // delay tag change detection to allow Tags to load
window.setTimeout(function () { window.setTimeout(() => {
self.canSaveTags = true this.canSaveTags = true
}, 200) }, 200)
}) })
}) })
}, },
methods: { methods: {
isHTMLTabSelected: function () { isHTMLTabSelected() {
this.showMobileButtons = this.$refs.navhtml this.showMobileButtons = this.$refs.navhtml
&& this.$refs.navhtml.classList.contains('active') && this.$refs.navhtml.classList.contains('active')
}, },
renderUI: function () { renderUI() {
let self = this
// activate the first non-disabled tab // activate the first non-disabled tab
document.querySelector('#nav-tab button:not([disabled])').click() document.querySelector('#nav-tab button:not([disabled])').click()
document.activeElement.blur() // blur focus document.activeElement.blur() // blur focus
document.getElementById('message-view').scrollTop = 0 document.getElementById('message-view').scrollTop = 0
self.isHTMLTabSelected() this.isHTMLTabSelected()
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(function (listObj) { document.querySelectorAll('button[data-bs-toggle="tab"]').forEach((listObj) => {
listObj.addEventListener('shown.bs.tab', function (event) { listObj.addEventListener('shown.bs.tab', (event) => {
self.isHTMLTabSelected() this.isHTMLTabSelected()
}) })
}) })
@ -135,7 +131,7 @@ export default {
[...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl)) [...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl))
// delay 0.2s until vue has rendered the iframe content // delay 0.2s until vue has rendered the iframe content
window.setTimeout(function () { window.setTimeout(() => {
let p = document.getElementById('preview-html') let p = document.getElementById('preview-html')
if (p) { if (p) {
// make links open in new window // make links open in new window
@ -148,7 +144,7 @@ export default {
anchorEl.setAttribute('target', '_blank') anchorEl.setAttribute('target', '_blank')
} }
} }
self.resizeIFrames() this.resizeIFrames()
} }
}, 200) }, 200)
@ -158,12 +154,12 @@ export default {
Prism.highlightAll() Prism.highlightAll()
}, },
resizeIframe: function (el) { resizeIframe(el) {
let i = el.target let i = el.target
i.style.height = i.contentWindow.document.body.scrollHeight + 50 + 'px' i.style.height = i.contentWindow.document.body.scrollHeight + 50 + 'px'
}, },
resizeIFrames: function () { resizeIFrames() {
if (this.scaleHTMLPreview != 'display') { if (this.scaleHTMLPreview != 'display') {
return return
} }
@ -175,7 +171,7 @@ export default {
}, },
// set the iframe body & text colors based on current theme // set the iframe body & text colors based on current theme
initRawIframe: function (el) { initRawIframe(el) {
let bodyStyles = window.getComputedStyle(document.body, null) let bodyStyles = window.getComputedStyle(document.body, null)
let bg = bodyStyles.getPropertyValue('background-color') let bg = bodyStyles.getPropertyValue('background-color')
let txt = bodyStyles.getPropertyValue('color') let txt = bodyStyles.getPropertyValue('color')
@ -189,27 +185,25 @@ export default {
this.resizeIframe(el) this.resizeIframe(el)
}, },
sanitizeHTML: function (h) { sanitizeHTML(h) {
// remove <base/> tag if set // remove <base/> tag if set
return h.replace(/<base .*>/mi, '') return h.replace(/<base .*>/mi, '')
}, },
saveTags: function () { saveTags() {
let self = this
var data = { var data = {
IDs: [this.message.ID], IDs: [this.message.ID],
Tags: this.messageTags Tags: this.messageTags
} }
self.put(self.resolve('/api/v1/tags'), data, function (response) { this.put(this.resolve('/api/v1/tags'), data, (response) => {
window.scrollInPlace = true window.scrollInPlace = true
self.$emit('loadMessages') this.$emit('loadMessages')
}) })
}, },
// Convert plain text to HTML including anchor links // Convert plain text to HTML including anchor links
textToHTML: function (s) { textToHTML(s) {
let html = s let html = s
// full links with http(s) // full links with http(s)

View File

@ -46,26 +46,25 @@ export default {
methods: { methods: {
// triggered manually after modal is shown // triggered manually after modal is shown
initTags: function () { initTags() {
Tags.init("select[multiple]") Tags.init("select[multiple]")
}, },
releaseMessage: function () { releaseMessage() {
let self = this
// set timeout to allow for user clicking send before the tag filter has applied the tag // set timeout to allow for user clicking send before the tag filter has applied the tag
window.setTimeout(function () { window.setTimeout(() => {
if (!self.addresses.length) { if (!this.addresses.length) {
return false return false
} }
let data = { let data = {
To: self.addresses To: this.addresses
} }
self.post(self.resolve('/api/v1/message/' + self.message.ID + '/release'), data, function (response) { this.post(this.resolve('/api/v1/message/' + this.message.ID + '/release'), data, (response) => {
self.modal("ReleaseModal").hide() this.modal("ReleaseModal").hide()
if (self.deleteAfterRelease) { if (this.deleteAfterRelease) {
self.$emit('delete') this.$emit('delete')
} }
}) })
}, 100) }, 100)

View File

@ -1,4 +1,3 @@
<script> <script>
import AjaxLoader from '../AjaxLoader.vue' import AjaxLoader from '../AjaxLoader.vue'
import CommonMixins from '../../mixins/CommonMixins' import CommonMixins from '../../mixins/CommonMixins'
@ -23,9 +22,8 @@ export default {
}, },
methods: { methods: {
initScreenshot: function () { initScreenshot() {
this.loading = 1 this.loading = 1
let self = this
// remove base tag, if set // remove base tag, if set
let h = this.message.HTML.replace(/<base .*>/mi, '') let h = this.message.HTML.replace(/<base .*>/mi, '')
let proxy = this.resolve('/proxy') let proxy = this.resolve('/proxy')
@ -38,11 +36,11 @@ export default {
// update any inline `url(...)` absolute links // update any inline `url(...)` absolute links
const urlRegex = /(url\((\'|\")?(https?:\/\/[^\)\'\"]+)(\'|\")?\))/mgi; const urlRegex = /(url\((\'|\")?(https?:\/\/[^\)\'\"]+)(\'|\")?\))/mgi;
h = h.replaceAll(urlRegex, function (match, p1, p2, p3) { h = h.replaceAll(urlRegex, (match, p1, p2, p3) => {
if (typeof p2 === 'string') { if (typeof p2 === 'string') {
return `url(${p2}${proxy}?url=` + encodeURIComponent(self.decodeEntities(p3)) + `${p2})` return `url(${p2}${proxy}?url=` + encodeURIComponent(this.decodeEntities(p3)) + `${p2})`
} }
return `url(${proxy}?url=` + encodeURIComponent(self.decodeEntities(p3)) + `)` return `url(${proxy}?url=` + encodeURIComponent(this.decodeEntities(p3)) + `)`
}) })
// create temporary document to manipulate // create temporary document to manipulate
@ -63,7 +61,7 @@ export default {
let src = i.getAttribute('href') let src = i.getAttribute('href')
if (src && src.match(/^https?:\/\//i) && src.indexOf(window.location.origin + window.location.pathname) !== 0) { if (src && src.match(/^https?:\/\//i) && src.indexOf(window.location.origin + window.location.pathname) !== 0) {
i.setAttribute('href', `${proxy}?url=` + encodeURIComponent(self.decodeEntities(src))) i.setAttribute('href', `${proxy}?url=` + encodeURIComponent(this.decodeEntities(src)))
} }
} }
@ -72,7 +70,7 @@ export default {
for (let i of images) { for (let i of images) {
let src = i.getAttribute('src') let src = i.getAttribute('src')
if (src && src.match(/^https?:\/\//i) && src.indexOf(window.location.origin + window.location.pathname) !== 0) { if (src && src.match(/^https?:\/\//i) && src.indexOf(window.location.origin + window.location.pathname) !== 0) {
i.setAttribute('src', `${proxy}?url=` + encodeURIComponent(self.decodeEntities(src))) i.setAttribute('src', `${proxy}?url=` + encodeURIComponent(this.decodeEntities(src)))
} }
} }
@ -83,7 +81,7 @@ export default {
if (src && src.match(/^https?:\/\//i) && src.indexOf(window.location.origin + window.location.pathname) !== 0) { if (src && src.match(/^https?:\/\//i) && src.indexOf(window.location.origin + window.location.pathname) !== 0) {
// replace with proxy link // replace with proxy link
i.setAttribute('background', `${proxy}?url=` + encodeURIComponent(self.decodeEntities(src))) i.setAttribute('background', `${proxy}?url=` + encodeURIComponent(this.decodeEntities(src)))
} }
} }
@ -92,7 +90,7 @@ export default {
}, },
// HTML decode function // HTML decode function
decodeEntities: function (s) { decodeEntities(s) {
let e = document.createElement('div') let e = document.createElement('div')
e.innerHTML = s e.innerHTML = s
let str = e.textContent let str = e.textContent
@ -100,8 +98,7 @@ export default {
return str return str
}, },
doScreenshot: function () { doScreenshot() {
let self = this
let width = document.getElementById('message-view').getBoundingClientRect().width let width = document.getElementById('message-view').getBoundingClientRect().width
let prev = document.getElementById('preview-html') let prev = document.getElementById('preview-html')
@ -113,7 +110,7 @@ export default {
width = 300 width = 300
} }
let i = document.getElementById('screenshot-html') const i = document.getElementById('screenshot-html')
// set the iframe width // set the iframe width
i.style.width = width + 'px' i.style.width = width + 'px'
@ -127,11 +124,11 @@ export default {
width: width, width: width,
}).then(dataUrl => { }).then(dataUrl => {
const link = document.createElement('a') const link = document.createElement('a')
link.download = self.message.ID + '.png' link.download = this.message.ID + '.png'
link.href = dataUrl link.href = dataUrl
link.click() link.click()
self.loading = 0 this.loading = 0
self.html = false this.html = false
}) })
} }
} }

View File

@ -38,41 +38,39 @@ export default {
}, },
methods: { methods: {
doCheck: function () { doCheck() {
this.check = false this.check = false
let self = this
// ignore any error, do not show loader // ignore any error, do not show loader
axios.get(self.resolve('/api/v1/message/' + self.message.ID + '/sa-check'), null) axios.get(this.resolve('/api/v1/message/' + this.message.ID + '/sa-check'), null)
.then(function (result) { .then((result) => {
self.check = result.data this.check = result.data
self.error = false this.error = false
self.setIcons() this.setIcons()
}) })
.catch(function (error) { .catch((error) => {
// handle error // handle error
if (error.response && error.response.data) { 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) {
self.error = error.response.data.Error this.error = error.response.data.Error
} else { } else {
self.error = error.response.data this.error = error.response.data
} }
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
self.error = 'Error sending data to the server. Please try again.' this.error = 'Error sending data to the server. Please try again.'
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
self.error = error.message this.error = error.message
} }
}) })
}, },
badgeStyle: function (ignorePadding = false) { badgeStyle(ignorePadding = false) {
let badgeStyle = 'bg-success' let badgeStyle = 'bg-success'
if (this.check.Error) { if (this.check.Error) {
badgeStyle = 'bg-warning text-primary' badgeStyle = 'bg-warning text-primary'
@ -90,7 +88,7 @@ export default {
return badgeStyle return badgeStyle
}, },
setIcons: function () { setIcons() {
let score = this.check.Score let score = this.check.Score
if (this.check.Error && this.check.Error != '') { if (this.check.Error && this.check.Error != '') {
score = '!' score = '!'
@ -102,7 +100,7 @@ export default {
}, },
computed: { computed: {
graphSections: function () { graphSections() {
let score = this.check.Score let score = this.check.Score
let p = Math.round(score / 5 * 100) let p = Math.round(score / 5 * 100)
if (p > 100) { if (p > 100) {
@ -125,7 +123,7 @@ export default {
] ]
}, },
scoreColor: function () { scoreColor() {
return this.graphSections[0].color return this.graphSections[0].color
}, },
} }

View File

@ -2,7 +2,7 @@ import axios from 'axios'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import ColorHash from 'color-hash' import ColorHash from 'color-hash'
import { Modal, Offcanvas } from 'bootstrap' import { Modal, Offcanvas } from 'bootstrap'
import {limitOptions} from "../stores/pagination"; import { limitOptions } from "../stores/pagination";
// BootstrapElement is used to return a fake Bootstrap element // BootstrapElement is used to return a fake Bootstrap element
// if the ID returns nothing to prevent errors. // if the ID returns nothing to prevent errors.
@ -25,15 +25,15 @@ export default {
}, },
methods: { methods: {
resolve: function (u) { resolve(u) {
return this.$router.resolve(u).href return this.$router.resolve(u).href
}, },
searchURI: function (s) { searchURI(s) {
return this.resolve('/search') + '?q=' + encodeURIComponent(s) return this.resolve('/search') + '?q=' + encodeURIComponent(s)
}, },
getFileSize: function (bytes) { getFileSize(bytes) {
if (bytes == 0) { if (bytes == 0) {
return '0B' return '0B'
} }
@ -41,19 +41,19 @@ export default {
return (bytes / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i] return (bytes / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
}, },
formatNumber: function (nr) { formatNumber(nr) {
return new Intl.NumberFormat().format(nr) return new Intl.NumberFormat().format(nr)
}, },
messageDate: function (d) { messageDate(d) {
return dayjs(d).format('ddd, D MMM YYYY, h:mm a') return dayjs(d).format('ddd, D MMM YYYY, h:mm a')
}, },
secondsToRelative: function (d) { secondsToRelative(d) {
return dayjs().subtract(d, 'seconds').fromNow() return dayjs().subtract(d, 'seconds').fromNow()
}, },
tagEncodeURI: function (tag) { tagEncodeURI(tag) {
if (tag.match(/ /)) { if (tag.match(/ /)) {
tag = `"${tag}"` tag = `"${tag}"`
} }
@ -61,7 +61,7 @@ export default {
return encodeURIComponent(`tag:${tag}`) return encodeURIComponent(`tag:${tag}`)
}, },
getSearch: function () { getSearch() {
if (!window.location.search) { if (!window.location.search) {
return false return false
} }
@ -75,7 +75,7 @@ export default {
return q return q
}, },
getPaginationParams: function () { getPaginationParams() {
if (!window.location.search) { if (!window.location.search) {
return null return null
} }
@ -90,8 +90,8 @@ export default {
}, },
// generic modal get/set function // generic modal get/set function
modal: function (id) { modal(id) {
let e = document.getElementById(id) const e = document.getElementById(id)
if (e) { if (e) {
return Modal.getOrCreateInstance(e) return Modal.getOrCreateInstance(e)
} }
@ -100,8 +100,8 @@ export default {
}, },
// close mobile navigation // close mobile navigation
hideNav: function () { hideNav() {
let e = document.getElementById('offcanvas') const e = document.getElementById('offcanvas')
if (e) { if (e) {
Offcanvas.getOrCreateInstance(e).hide() Offcanvas.getOrCreateInstance(e).hide()
} }
@ -115,22 +115,21 @@ export default {
* @params function callback function * @params function callback function
* @params function error callback function * @params function error callback function
*/ */
get: function (url, values, callback, errorCallback) { get(url, values, callback, errorCallback) {
let self = this this.loading++
self.loading++
axios.get(url, { params: values }) axios.get(url, { params: values })
.then(callback) .then(callback)
.catch(function (err) { .catch((err) => {
if (typeof errorCallback == 'function') { if (typeof errorCallback == 'function') {
return errorCallback(err) return errorCallback(err)
} }
self.handleError(err) this.handleError(err)
}) })
.then(function () { .then(() => {
// always executed // always executed
if (self.loading > 0) { if (this.loading > 0) {
self.loading-- this.loading--
} }
}) })
}, },
@ -142,16 +141,15 @@ export default {
* @params array object/array values * @params array object/array values
* @params function callback function * @params function callback function
*/ */
post: function (url, data, callback) { post(url, data, callback) {
let self = this this.loading++
self.loading++
axios.post(url, data) axios.post(url, data)
.then(callback) .then(callback)
.catch(self.handleError) .catch(this.handleError)
.then(function () { .then(() => {
// always executed // always executed
if (self.loading > 0) { if (this.loading > 0) {
self.loading-- this.loading--
} }
}) })
}, },
@ -163,16 +161,15 @@ export default {
* @params array object/array values * @params array object/array values
* @params function callback function * @params function callback function
*/ */
delete: function (url, data, callback) { delete(url, data, callback) {
let self = this this.loading++
self.loading++
axios.delete(url, { data: data }) axios.delete(url, { data: data })
.then(callback) .then(callback)
.catch(self.handleError) .catch(this.handleError)
.then(function () { .then(() => {
// always executed // always executed
if (self.loading > 0) { if (this.loading > 0) {
self.loading-- this.loading--
} }
}) })
}, },
@ -184,22 +181,21 @@ export default {
* @params array object/array values * @params array object/array values
* @params function callback function * @params function callback function
*/ */
put: function (url, data, callback) { put(url, data, callback) {
let self = this this.loading++
self.loading++
axios.put(url, data) axios.put(url, data)
.then(callback) .then(callback)
.catch(self.handleError) .catch(this.handleError)
.then(function () { .then(() => {
// always executed // always executed
if (self.loading > 0) { if (this.loading > 0) {
self.loading-- this.loading--
} }
}) })
}, },
// Ajax error message // Ajax error message
handleError: function (error) { handleError(error) {
// handle error // handle error
if (error.response && error.response.data) { 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
@ -218,7 +214,7 @@ export default {
} }
}, },
allAttachments: function (message) { allAttachments(message) {
let a = [] let a = []
for (let i in message.Attachments) { for (let i in message.Attachments) {
a.push(message.Attachments[i]) a.push(message.Attachments[i])
@ -237,7 +233,7 @@ export default {
return a.ContentType.match(/^image\//) return a.ContentType.match(/^image\//)
}, },
attachmentIcon: function (a) { attachmentIcon(a) {
let ext = a.FileName.split('.').pop().toLowerCase() let ext = a.FileName.split('.').pop().toLowerCase()
if (a.ContentType.match(/^image\//)) { if (a.ContentType.match(/^image\//)) {
@ -279,7 +275,7 @@ export default {
// Returns a hex color based on a string. // Returns a hex color based on a string.
// Values are stored in an array for faster lookup / processing. // Values are stored in an array for faster lookup / processing.
colorHash: function (s) { colorHash(s) {
if (this.tagColorCache[s] != undefined) { if (this.tagColorCache[s] != undefined) {
return this.tagColorCache[s] return this.tagColorCache[s]
} }

View File

@ -25,12 +25,12 @@ export default {
}, },
methods: { methods: {
reloadMailbox: function () { reloadMailbox() {
pagination.start = 0 pagination.start = 0
this.loadMessages() this.loadMessages()
}, },
loadMessages: function () { loadMessages() {
if (!this.apiURI) { if (!this.apiURI) {
alert('apiURL not set!') alert('apiURL not set!')
return return
@ -43,8 +43,7 @@ export default {
return return
} }
let self = this const params = {}
let params = {}
mailbox.selected = [] mailbox.selected = []
params['limit'] = pagination.limit params['limit'] = pagination.limit
@ -52,7 +51,7 @@ export default {
params['start'] = pagination.start params['start'] = pagination.start
} }
self.get(this.apiURI, params, function (response) { this.get(this.apiURI, params, (response) => {
mailbox.total = response.data.total // all messages mailbox.total = response.data.total // all messages
mailbox.unread = response.data.unread // all unread messages mailbox.unread = response.data.unread // all unread messages
mailbox.tags = response.data.tags // all tags mailbox.tags = response.data.tags // all tags
@ -63,18 +62,18 @@ export default {
if (response.data.count == 0 && response.data.start > 0) { if (response.data.count == 0 && response.data.start > 0) {
pagination.start = 0 pagination.start = 0
return self.loadMessages() return this.loadMessages()
} }
if (mailbox.lastMessage) { if (mailbox.lastMessage) {
window.setTimeout(() => { window.setTimeout(() => {
let m = document.getElementById(mailbox.lastMessage) const m = document.getElementById(mailbox.lastMessage)
if (m) { if (m) {
m.focus() m.focus()
// m.scrollIntoView({ behavior: 'smooth', block: 'center' }) // m.scrollIntoView({ behavior: 'smooth', block: 'center' })
m.scrollIntoView({ block: 'center' }) m.scrollIntoView({ block: 'center' })
} else { } else {
let mp = document.getElementById('message-page') const mp = document.getElementById('message-page')
if (mp) { if (mp) {
mp.scrollTop = 0 mp.scrollTop = 0
} }
@ -84,7 +83,7 @@ export default {
}, 50) }, 50)
} else if (!window.scrollInPlace) { } else if (!window.scrollInPlace) {
let mp = document.getElementById('message-page') const mp = document.getElementById('message-page')
if (mp) { if (mp) {
mp.scrollTop = 0 mp.scrollTop = 0
} }

View File

@ -43,7 +43,7 @@ export default {
}, },
methods: { methods: {
loadMailbox: function () { loadMailbox() {
const paginationParams = this.getPaginationParams() const paginationParams = this.getPaginationParams()
if (paginationParams?.start) { if (paginationParams?.start) {
pagination.start = paginationParams.start pagination.start = paginationParams.start

View File

@ -41,16 +41,14 @@ export default {
}, },
methods: { methods: {
loadMessage: function () { loadMessage() {
let self = this
this.message = false this.message = false
let uri = self.resolve('/api/v1/message/' + this.$route.params.id) const uri = this.resolve('/api/v1/message/' + this.$route.params.id)
self.get(uri, false, function (response) { this.get(uri, false, (response) => {
self.errorMessage = false this.errorMessage = false
const d = response.data
let d = response.data if (this.wasUnread(d.ID)) {
if (self.wasUnread(d.ID)) {
mailbox.unread-- mailbox.unread--
} }
@ -61,14 +59,14 @@ export default {
if (a.ContentID != '') { if (a.ContentID != '') {
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\\s|\\/|>|;])', 'g'), new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\\s|\\/|>|;])', 'g'),
'$1' + self.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3' '$1' + this.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3'
) )
} }
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) { if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
// some old email clients use the filename // some old email clients use the filename
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\\s|\\/|>|;])', 'g'), new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\\s|\\/|>|;])', 'g'),
'$1' + self.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3' '$1' + this.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3'
) )
} }
} }
@ -81,43 +79,43 @@ export default {
if (a.ContentID != '') { if (a.ContentID != '') {
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\\s|\\/|>|;])', 'g'), new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\\s|\\/|>|;])', 'g'),
'$1' + self.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3' '$1' + this.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3'
) )
} }
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) { if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
// some old email clients use the filename // some old email clients use the filename
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\\s|\\/|>|;])', 'g'), new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\\s|\\/|>|;])', 'g'),
'$1' + self.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3' '$1' + this.resolve('/api/v1/message/' + d.ID + '/part/' + a.PartID) + '$3'
) )
} }
} }
} }
self.message = d this.message = d
self.detectPrevNext() this.detectPrevNext()
}, },
function (error) { (error) => {
self.errorMessage = true this.errorMessage = true
if (error.response && error.response.data) { if (error.response && error.response.data) {
if (error.response.data.Error) { if (error.response.data.Error) {
self.errorMessage = error.response.data.Error this.errorMessage = error.response.data.Error
} else { } else {
self.errorMessage = error.response.data this.errorMessage = error.response.data
} }
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
self.errorMessage = 'Error sending data to the server. Please refresh the page.' this.errorMessage = 'Error sending data to the server. Please refresh the page.'
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
self.errorMessage = error.message this.errorMessage = error.message
} }
}) })
}, },
// try detect whether this message was unread based on messages listing // try detect whether this message was unread based on messages listing
wasUnread: function (id) { wasUnread(id) {
for (let m in mailbox.messages) { for (let m in mailbox.messages) {
if (mailbox.messages[m].ID == id) { if (mailbox.messages[m].ID == id) {
if (!mailbox.messages[m].Read) { if (!mailbox.messages[m].Read) {
@ -129,7 +127,7 @@ export default {
} }
}, },
detectPrevNext: function () { detectPrevNext() {
// generate the prev/next links based on current message list // generate the prev/next links based on current message list
this.prevLink = false this.prevLink = false
this.nextLink = false this.nextLink = false
@ -147,40 +145,38 @@ export default {
} }
}, },
downloadMessageBody: function (str, ext) { downloadMessageBody(str, ext) {
let dl = document.createElement('a') const dl = document.createElement('a')
dl.href = "data:text/plain," + encodeURIComponent(str) dl.href = "data:text/plain," + encodeURIComponent(str)
dl.target = '_blank' dl.target = '_blank'
dl.download = this.message.ID + '.' + ext dl.download = this.message.ID + '.' + ext
dl.click() dl.click()
}, },
screenshotMessageHTML: function () { screenshotMessageHTML() {
this.$refs.ScreenshotRef.initScreenshot() this.$refs.ScreenshotRef.initScreenshot()
}, },
// mark current message as read // mark current message as read
markUnread: function () { markUnread() {
let self = this if (!this.message) {
if (!self.message) {
return false return false
} }
let uri = self.resolve('/api/v1/messages') const uri = this.resolve('/api/v1/messages')
self.put(uri, { 'read': false, 'ids': [self.message.ID] }, function (response) { this.put(uri, { 'read': false, 'ids': [this.message.ID] }, (response) => {
self.goBack() this.goBack()
}) })
}, },
deleteMessage: function () { deleteMessage() {
let self = this const ids = [this.message.ID]
let ids = [self.message.ID] const uri = this.resolve('/api/v1/messages')
let uri = self.resolve('/api/v1/messages') this.delete(uri, { 'ids': ids }, () => {
self.delete(uri, { 'ids': ids }, function () { this.goBack()
self.goBack()
}) })
}, },
goBack: function () { goBack() {
mailbox.lastMessage = this.$route.params.id mailbox.lastMessage = this.$route.params.id
if (mailbox.searching) { if (mailbox.searching) {
@ -208,16 +204,13 @@ export default {
} }
}, },
initReleaseModal: function () { initReleaseModal() {
let self = this this.modal('ReleaseModal').show()
self.modal('ReleaseModal').show() window.setTimeout(() => {
window.setTimeout(function () { // delay to allow elements to load / focus
window.setTimeout(function () { this.$refs.ReleaseRef.initTags()
// delay to allow elements to load / focus document.querySelector('#ReleaseModal input[role="combobox"]').focus()
self.$refs.ReleaseRef.initTags() }, 500)
document.querySelector('#ReleaseModal input[role="combobox"]').focus()
}, 500)
}, 300)
}, },
} }
} }

View File

@ -43,8 +43,8 @@ export default {
}, },
methods: { methods: {
doSearch: function () { doSearch() {
let s = this.getSearch() const s = this.getSearch()
if (!s) { if (!s) {
mailbox.searching = false mailbox.searching = false