mirror of
https://github.com/axllent/mailpit.git
synced 2025-06-27 00:41:24 +02:00
@ -39,14 +39,13 @@
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
@include media-breakpoint-down(sm) {
|
||||
// font-size: 14px;
|
||||
@include media-breakpoint-down(xl) {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:not(.text-view) > a {
|
||||
:not(.text-view) > a:not(.no-icon) {
|
||||
&[href^="http://"],
|
||||
&[href^="https://"]
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ import Tags from "bootstrap5-tags"
|
||||
import Attachments from './Attachments.vue'
|
||||
import Headers from './Headers.vue'
|
||||
import HTMLCheck from './MessageHTMLCheck.vue'
|
||||
import LinkCheck from './MessageLinkCheck.vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -18,6 +19,7 @@ export default {
|
||||
Attachments,
|
||||
Headers,
|
||||
HTMLCheck,
|
||||
LinkCheck,
|
||||
},
|
||||
|
||||
mixins: [commonMixins],
|
||||
@ -33,6 +35,7 @@ export default {
|
||||
loadHeaders: false,
|
||||
htmlScore: false,
|
||||
htmlScoreColor: false,
|
||||
linkCheckErrors: false,
|
||||
showMobileButtons: false,
|
||||
scaleHTMLPreview: 'display',
|
||||
// keys names match bootstrap icon names
|
||||
@ -219,7 +222,7 @@ export default {
|
||||
let html = s
|
||||
|
||||
// full links with http(s)
|
||||
let re = /(\b(https?|ftp):\/\/[\-\w@:%_\+.~#?,&\/\/=;]+)\b/gim
|
||||
let re = /(\b(https?|ftp):\/\/[\-\w@:%_\+'!.~#?,&\/\/=;]+)/gim
|
||||
html = html.replace(re, '˱˱˱a href=ˠˠˠ$&ˠˠˠ target=_blank rel=noopener˲˲˲$&˱˱˱/a˲˲˲')
|
||||
|
||||
// plain www links without https?:// prefix
|
||||
@ -366,15 +369,51 @@ export default {
|
||||
role="tab" aria-controls="nav-raw" aria-selected="false">
|
||||
Raw
|
||||
</button>
|
||||
<button class="nav-link position-relative" id="nav-html-check-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-html-check" type="button" role="tab" aria-controls="nav-html" aria-selected="false"
|
||||
v-if="!uiConfig.DisableHTMLCheck && message.HTML != ''" @click="showMobileButtons = false">
|
||||
<span class="d-none d-sm-inline">HTML</span> Check
|
||||
<span class="position-absolute top-10 start-100 translate-middle badge rounded-pill p-1"
|
||||
:class="htmlScoreColor" v-if="htmlScore !== false">
|
||||
<div class="dropdown d-lg-none">
|
||||
<button class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Checks
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<button class="dropdown-item" id="nav-html-check-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-html-check" type="button" role="tab" aria-controls="nav-html"
|
||||
aria-selected="false" v-if="!uiConfig.DisableHTMLCheck && message.HTML != ''">
|
||||
HTML Check
|
||||
<span class="badge rounded-pill p-1" :class="htmlScoreColor" v-if="htmlScore !== false">
|
||||
<small>{{ Math.floor(htmlScore) }}%</small>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item" id="nav-link-check-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-link-check" type="button" role="tab" aria-controls="nav-link-check"
|
||||
aria-selected="false">
|
||||
Link Check
|
||||
<i class="bi bi-check-all text-success" v-if="linkCheckErrors === 0"></i>
|
||||
<span class="badge rounded-pill bg-danger" v-else-if="linkCheckErrors > 0">
|
||||
<small>{{ formatNumber(linkCheckErrors) }}</small>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="d-none d-lg-inline-block nav-link position-relative" id="nav-html-check-tab"
|
||||
data-bs-toggle="tab" data-bs-target="#nav-html-check" type="button" role="tab" aria-controls="nav-html"
|
||||
aria-selected="false" v-if="!uiConfig.DisableHTMLCheck && message.HTML != ''">
|
||||
HTML Check
|
||||
<span class="badge rounded-pill p-1" :class="htmlScoreColor" v-if="htmlScore !== false">
|
||||
<small>{{ Math.floor(htmlScore) }}%</small>
|
||||
</span>
|
||||
</button>
|
||||
<button class="d-none d-lg-inline-block nav-link" id="nav-link-check-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#nav-link-check" type="button" role="tab" aria-controls="nav-link-check"
|
||||
aria-selected="false">
|
||||
Link Check
|
||||
<i class="bi bi-check-all text-success" v-if="linkCheckErrors === 0"></i>
|
||||
<span class="badge rounded-pill bg-danger" v-else-if="linkCheckErrors > 0">
|
||||
<small>{{ formatNumber(linkCheckErrors) }}</small>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="d-none d-lg-block ms-auto me-3" v-if="showMobileButtons">
|
||||
<template v-for="vals, key in responsiveSizes">
|
||||
@ -398,11 +437,6 @@ export default {
|
||||
<Attachments v-if="allAttachments(message).length" :message="message"
|
||||
:attachments="allAttachments(message)"></Attachments>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-html-check" role="tabpanel" aria-labelledby="nav-html-check-tab"
|
||||
tabindex="0">
|
||||
<HTMLCheck v-if="!uiConfig.DisableHTMLCheck && message.HTML != ''" :message="message"
|
||||
@setHtmlScore="(n) => htmlScore = n" @set-badge-style="(v) => htmlScoreColor = v" />
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-html-source" role="tabpanel" aria-labelledby="nav-html-source-tab"
|
||||
tabindex="0" v-if="message.HTML">
|
||||
<pre><code class="language-html">{{ message.HTML }}</code></pre>
|
||||
@ -420,6 +454,15 @@ export default {
|
||||
<iframe v-if="srcURI" :src="srcURI" v-on:load="initRawIframe" frameborder="0"
|
||||
style="width: 100%; height: 300px"></iframe>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-html-check" role="tabpanel" aria-labelledby="nav-html-check-tab"
|
||||
tabindex="0">
|
||||
<HTMLCheck v-if="!uiConfig.DisableHTMLCheck && message.HTML != ''" :message="message"
|
||||
@setHtmlScore="(n) => htmlScore = n" @set-badge-style="(v) => htmlScoreColor = v" />
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-link-check" role="tabpanel" aria-labelledby="nav-html-check-tab"
|
||||
tabindex="0">
|
||||
<LinkCheck :message="message" @setLinkErrors="(n) => linkCheckErrors = n" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -640,7 +640,7 @@ export default {
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="HTMLCheckOptionsLabel">About HTML check</h1>
|
||||
<h1 class="modal-title fs-5" id="HTMLCheckOptionsLabel">HTML check options</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
398
server/ui-src/templates/MessageLinkCheck.vue
Normal file
398
server/ui-src/templates/MessageLinkCheck.vue
Normal file
@ -0,0 +1,398 @@
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import commonMixins from '../mixins.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: Object,
|
||||
},
|
||||
|
||||
emits: ["setLinkErrors"],
|
||||
|
||||
mixins: [commonMixins],
|
||||
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
autoScan: false,
|
||||
followRedirects: false,
|
||||
check: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.autoScan = localStorage.getItem('LinkCheckAutoScan')
|
||||
this.followRedirects = localStorage.getItem('LinkCheckFollowRedirects')
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loaded = true
|
||||
if (this.autoScan) {
|
||||
this.doCheck()
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
autoScan(v) {
|
||||
if (!this.loaded) {
|
||||
return
|
||||
}
|
||||
if (v) {
|
||||
localStorage.setItem('LinkCheckAutoScan', true)
|
||||
if (!this.check) {
|
||||
this.doCheck()
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('LinkCheckAutoScan')
|
||||
}
|
||||
},
|
||||
followRedirects(v) {
|
||||
if (!this.loaded) {
|
||||
return
|
||||
}
|
||||
if (v) {
|
||||
localStorage.setItem('LinkCheckFollowRedirects', true)
|
||||
} else {
|
||||
localStorage.removeItem('LinkCheckFollowRedirects')
|
||||
}
|
||||
if (this.check) {
|
||||
this.doCheck()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
groupedStatuses: function () {
|
||||
let results = {}
|
||||
|
||||
if (!this.check) {
|
||||
return results
|
||||
}
|
||||
|
||||
// group by status
|
||||
this.check.Links.forEach(function (r) {
|
||||
if (!results[r.StatusCode]) {
|
||||
let css = ""
|
||||
if (r.StatusCode >= 400 || r.StatusCode === 0) {
|
||||
css = "text-danger"
|
||||
} else if (r.StatusCode >= 300) {
|
||||
css = "text-info"
|
||||
}
|
||||
|
||||
if (r.StatusCode === 0) {
|
||||
r.Status = 'Cannot connect to server'
|
||||
}
|
||||
results[r.StatusCode] = {
|
||||
StatusCode: r.StatusCode,
|
||||
Status: r.Status,
|
||||
Class: css,
|
||||
URLS: []
|
||||
}
|
||||
}
|
||||
results[r.StatusCode].URLS.push(r.URL)
|
||||
})
|
||||
|
||||
let newArr = []
|
||||
|
||||
for (const i in results) {
|
||||
newArr.push(results[i])
|
||||
}
|
||||
|
||||
// sort statuses
|
||||
let sorted = newArr.sort((a, b) => {
|
||||
if (a.StatusCode === 0) {
|
||||
return false
|
||||
}
|
||||
return a.StatusCode < b.StatusCode
|
||||
})
|
||||
|
||||
|
||||
return sorted
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
doCheck: function () {
|
||||
this.check = false
|
||||
let self = this
|
||||
this.loading = true
|
||||
let uri = 'api/v1/message/' + self.message.ID + '/link-check'
|
||||
if (this.followRedirects) {
|
||||
uri += '?follow=true'
|
||||
}
|
||||
|
||||
// ignore any error, do not show loader
|
||||
axios.get(uri, null)
|
||||
.then(function (result) {
|
||||
self.check = result.data
|
||||
self.error = false
|
||||
|
||||
self.$emit('setLinkErrors', result.data.Errors)
|
||||
})
|
||||
.catch(function (error) {
|
||||
// handle error
|
||||
if (error.response && error.response.data) {
|
||||
// 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) {
|
||||
self.error = error.response.data.Error
|
||||
} else {
|
||||
self.error = error.response.data
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
self.error = 'Error sending data to the server. Please try again.'
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
self.error = error.message
|
||||
}
|
||||
})
|
||||
.then(function (result) {
|
||||
// always run
|
||||
self.loading = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pe-3">
|
||||
<div class="row mb-3 align-items-center">
|
||||
<div class="col">
|
||||
<h4 class="mb-0">
|
||||
<template v-if="!check">
|
||||
Link check
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="check.Links.length">
|
||||
Scanned {{ formatNumber(check.Links.length) }}
|
||||
link<template v-if="check.Links.length != 1">s</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
No links detected
|
||||
</template>
|
||||
</template>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" data-bs-toggle="modal"
|
||||
data-bs-target="#AboutLinkCheckResults">
|
||||
<i class="bi bi-info-circle-fill"></i>
|
||||
Help
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#LinkCheckOptions">
|
||||
<i class="bi bi-gear-fill"></i>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!check">
|
||||
<p class="text-secondary">
|
||||
Link check scans your email text & HTML for unique links, testing the response status codes.
|
||||
This includes links to images and remote CSS stylesheets.
|
||||
</p>
|
||||
|
||||
<p class="text-center my-5">
|
||||
<button v-if="!check" class="btn btn-primary btn-lg" @click="doCheck()" :disabled="loading">
|
||||
<template v-if="loading">
|
||||
Checking links
|
||||
<div class="ms-1 spinner-border spinner-border-sm text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="bi bi-check-square me-2"></i>
|
||||
Check message links
|
||||
</template>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else v-for="s, k in groupedStatuses">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header h4" :class="s.Class">
|
||||
Status {{ s.StatusCode }}
|
||||
<small v-if="s.Status != ''" class="ms-2 small text-secondary">({{ s.Status }})</small>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li v-for="u in s.URLS" class="list-group-item">
|
||||
<a :href="u" target="_blank" class="no-icon">{{ u }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="error">
|
||||
<p>Link check failed to load:</p>
|
||||
<div class="alert alert-warning">
|
||||
{{ error }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<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-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="LinkCheckOptionsLabel">Link check options</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Link check is currently in beta. Constructive feedback is welcome via
|
||||
<a href="https://github.com/axllent/mailpit/issues" target="_blank">GitHub</a>.
|
||||
</p>
|
||||
|
||||
<h6 class="mt-4">Follow HTTP redirects (status 301 & 302)</h6>
|
||||
<div class="form-check form-switch mb-4">
|
||||
<input class="form-check-input" type="checkbox" role="switch" v-model="followRedirects"
|
||||
id="LinkCheckFollowRedirectsSwitch">
|
||||
<label class="form-check-label" for="LinkCheckFollowRedirectsSwitch">
|
||||
<template v-if="followRedirects">Following HTTP redirects</template>
|
||||
<template v-else>Not following HTTP redirects</template>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4">Automatic link checking</h6>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" role="switch" v-model="autoScan"
|
||||
id="LinkCheckAutoCheckSwitch">
|
||||
<label class="form-check-label" for="LinkCheckAutoCheckSwitch">
|
||||
<template v-if="autoScan">Automatic link checking is enabled</template>
|
||||
<template v-else>Automatic link checking is disabled</template>
|
||||
</label>
|
||||
<div class="form-text">
|
||||
Note: Enabling auto checking will scan every link & image every time a message is opened.
|
||||
Only enable this if you understand the potential risks & consequences.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="AboutLinkCheckResults" tabindex="-1" aria-labelledby="AboutLinkCheckResultsLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="AboutLinkCheckResultsLabel">About Link check</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Link check is currently in beta. Constructive feedback is welcome via
|
||||
<a href="https://github.com/axllent/mailpit/issues" target="_blank">GitHub</a>.
|
||||
</p>
|
||||
<div class="accordion" id="LinkCheckAboutAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#col1" aria-expanded="false" aria-controls="col1">
|
||||
What is Link check?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="col1" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion">
|
||||
<div class="accordion-body">
|
||||
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
|
||||
test whether the link/image/stylesheet exists.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#col2" aria-expanded="false" aria-controls="col2">
|
||||
What are "301" and "302" links?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="col2" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion">
|
||||
<div class="accordion-body">
|
||||
<p>
|
||||
These are links that redirect you to another URL, for example newsletters
|
||||
often use redirect links to track user clicks.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#col3" aria-expanded="false" aria-controls="col3">
|
||||
Why are some links returning an error but work in my browser?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="col3" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion">
|
||||
<div class="accordion-body">
|
||||
<p>This may be due to various reasons, for instance:</p>
|
||||
<ul>
|
||||
<li>The Mailpit server cannot resolve (DNS) the hostname of the URL.</li>
|
||||
<li>Mailpit is not allowed to access the URL.</li>
|
||||
<li>
|
||||
The webserver is blocking requests that don't come from authenticated web
|
||||
browsers.
|
||||
</li>
|
||||
<li>The webserver or doesn't allow HTTP <code>HEAD</code> requests. </li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#col4" aria-expanded="false" aria-controls="col4">
|
||||
What are the risks of running Link check automatically?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="col4" class="accordion-collapse collapse" data-bs-parent="#LinkCheckAboutAccordion">
|
||||
<div class="accordion-body">
|
||||
<p>
|
||||
Depending on the type of messages you are testing, opening all links on all messages
|
||||
may have undesired consequences:
|
||||
</p>
|
||||
<ul>
|
||||
<li>If the message contains tracking links this may reveal your identity.</li>
|
||||
<li>
|
||||
If the message contains unsubscribe links, Link check could unintentionally
|
||||
unsubscribe you.
|
||||
</li>
|
||||
<li>
|
||||
To speed up the checking process, Link check will attempt 5 URLs at a time. This
|
||||
could lead to temporary heady load on the remote server.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Unless you know what messages you receive, it is advised to only run the Link check
|
||||
manually.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
Reference in New Issue
Block a user