diff --git a/package-lock.json b/package-lock.json
index f01e66e..eefaf5f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36,9 +36,9 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.22.4",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz",
-      "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==",
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
+      "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -47,9 +47,9 @@
       }
     },
     "node_modules/@babel/runtime-corejs3": {
-      "version": "7.22.3",
-      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.22.3.tgz",
-      "integrity": "sha512-6bdmknScYKmt8I9VjsJuKKGr+TwUb555FTf6tT1P/ANlCjTHCiYLhiQ4X/O7J731w5NOqu8c1aYHEVuOwPz7jA==",
+      "version": "7.22.5",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.22.5.tgz",
+      "integrity": "sha512-TNPDN6aBFaUox2Lu+H/Y1dKKQgr4ucz/FGyCz67RVYLsBpVpUFf1dDngzg+Od8aqbrqwyztkaZjtWCZEUOT8zA==",
       "dependencies": {
         "core-js-pure": "^3.30.2",
         "regenerator-runtime": "^0.13.11"
@@ -421,9 +421,9 @@
       "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ=="
     },
     "node_modules/@lit/reactive-element": {
-      "version": "1.6.1",
-      "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.1.tgz",
-      "integrity": "sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==",
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.2.tgz",
+      "integrity": "sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA==",
       "dependencies": {
         "@lit-labs/ssr-dom-shim": "^1.0.0"
       }
@@ -1063,9 +1063,9 @@
       ]
     },
     "node_modules/bootstrap5-tags": {
-      "version": "1.5.22",
-      "resolved": "https://registry.npmjs.org/bootstrap5-tags/-/bootstrap5-tags-1.5.22.tgz",
-      "integrity": "sha512-wRpPtBBql6FvXKdSKUgFBkmmkGgsZJCzZ0bC9DvfHKylnQ4gS2pHpXB2UkIEWa/c+vENbBIyUDMSAvcZ4ujyQg=="
+      "version": "1.5.24",
+      "resolved": "https://registry.npmjs.org/bootstrap5-tags/-/bootstrap5-tags-1.5.24.tgz",
+      "integrity": "sha512-+mH9WfNnz81CFoyui8G166YLe0ASEiSPb17DMdejCVdIOu7YGuSmYFfCAz/oqdD09vKege9yU6/YyXPOlGURGQ=="
     },
     "node_modules/brace-expansion": {
       "version": "2.0.1",
@@ -1201,9 +1201,9 @@
       }
     },
     "node_modules/core-js-pure": {
-      "version": "3.30.2",
-      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.30.2.tgz",
-      "integrity": "sha512-p/npFUJXXBkCCTIlEGBdghofn00jWG6ZOtdoIXSJmAu2QBvN0IqpZXWweOytcwE6cfx8ZvVUy1vw8zxhe4Y2vg==",
+      "version": "3.31.0",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.31.0.tgz",
+      "integrity": "sha512-/AnE9Y4OsJZicCzIe97JP5XoPKQJfTuEG43aEVLFJGOJpyqELod+pE6LEl63DfG1Mp8wX97LDaDpy1GmLEUxlg==",
       "hasInstallScript": true,
       "funding": {
         "type": "opencollective",
@@ -1683,9 +1683,9 @@
       }
     },
     "node_modules/lit": {
-      "version": "2.7.4",
-      "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.4.tgz",
-      "integrity": "sha512-cgD7xrZoYr21mbrkZIuIrj98YTMw/snJPg52deWVV4A8icLyNHI3bF70xsJeAgwTuiq5Kkd+ZR8gybSJDCPB7g==",
+      "version": "2.7.5",
+      "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.5.tgz",
+      "integrity": "sha512-i/cH7Ye6nBDUASMnfwcictBnsTN91+aBjXoTHF2xARghXScKxpD4F4WYI+VLXg9lqbMinDfvoI7VnZXjyHgdfQ==",
       "dependencies": {
         "@lit/reactive-element": "^1.6.0",
         "lit-element": "^3.3.0",
@@ -2062,9 +2062,9 @@
       }
     },
     "node_modules/punycode": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
-      "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
     },
     "node_modules/qs": {
       "version": "6.11.2",
@@ -2080,15 +2080,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/querystring": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
-      "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
-      "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
-      "engines": {
-        "node": ">=0.4.x"
-      }
-    },
     "node_modules/ramda": {
       "version": "0.29.0",
       "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz",
@@ -2209,9 +2200,9 @@
       "optional": true
     },
     "node_modules/sass": {
-      "version": "1.62.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
-      "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
+      "version": "1.63.4",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.4.tgz",
+      "integrity": "sha512-Sx/+weUmK+oiIlI+9sdD0wZHsqpbgQg8wSwSnGBjwb5GwqFhYNwwnI+UWZtLjKvKyFlKkatRK235qQ3mokyPoQ==",
       "dev": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
@@ -2526,12 +2517,12 @@
       "integrity": "sha512-tdOvLfRzHolwYcHS6HIX860MkK9LQ4+oLuNwFYL7bpgTEO64PZrcQxkisgwJYCfF8sKiWLwwu1c83DvMkbefIQ=="
     },
     "node_modules/url": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
-      "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==",
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz",
+      "integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==",
       "dependencies": {
-        "punycode": "1.3.2",
-        "querystring": "0.2.0"
+        "punycode": "^1.4.1",
+        "qs": "^6.11.0"
       }
     },
     "node_modules/util-deprecate": {
diff --git a/server/ui-src/templates/Message.vue b/server/ui-src/templates/Message.vue
index a0919d6..f212f4b 100644
--- a/server/ui-src/templates/Message.vue
+++ b/server/ui-src/templates/Message.vue
@@ -163,6 +163,27 @@ export default {
 				self.scrollInPlace = true;
 				self.$emit('loadMessages');
 			});
+		},
+
+		// Convert plain text to HTML including anchor links
+		textToHTML: function (s) {
+			// escape to HTML
+			let html = s
+				.replace(/&/g, "&amp;")
+				.replace(/</g, "&lt;")
+				.replace(/>/g, "&gt;")
+				.replace(/"/g, "&quot;")
+				.replace(/'/g, "&#039;")
+
+			// full links with http(s)
+			let re = /(\b(https?|ftp):\/\/[\-\w@:%_\+.~#?,&\/\/=;]+)\b/gim
+			html = html.replace(re, '<a href="$&" target="_blank" rel="noopener">$&</a>')
+
+			// plain www links without https?:// prefix
+			let re2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim
+			html = html.replace(re2, '$1<a href="http://$2" target="_blank" rel="noopener">$2</a>')
+
+			return html
 		}
 	}
 }
@@ -272,28 +293,28 @@ export default {
 					role="tab" aria-controls="nav-html" aria-selected="true" v-if="message.HTML"
 					v-on:click="showMobileBtns = true; resizeIframes()">HTML</button>
 				<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab" data-bs-target="#nav-html-source"
-					type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" v-if=" message.HTML "
-					v-on:click=" showMobileBtns = false ">
+					type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" v-if="message.HTML"
+					v-on:click=" showMobileBtns = false">
 					HTML <span class="d-sm-none">Src</span><span class="d-none d-sm-inline">Source</span>
 				</button>
 				<button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab" data-bs-target="#nav-plain-text"
 					type="button" role="tab" aria-controls="nav-plain-text" aria-selected="false"
-					:class=" message.HTML == '' ? 'show' : '' " v-on:click=" showMobileBtns = false ">Text</button>
+					:class="message.HTML == '' ? 'show' : ''" v-on:click=" showMobileBtns = false">Text</button>
 				<button class="nav-link" id="nav-headers-tab" data-bs-toggle="tab" data-bs-target="#nav-headers"
 					type="button" role="tab" aria-controls="nav-headers" aria-selected="false"
-					v-on:click=" showMobileBtns = false ">
+					v-on:click=" showMobileBtns = false">
 					<span class="d-sm-none">Hdrs</span><span class="d-none d-sm-inline">Headers</span>
 				</button>
 				<button class="nav-link" id="nav-raw-tab" data-bs-toggle="tab" data-bs-target="#nav-raw" type="button"
 					role="tab" aria-controls="nav-raw" aria-selected="false"
-					v-on:click=" showMobileBtns = false ">Raw</button>
+					v-on:click=" showMobileBtns = false">Raw</button>
 
-				<div class="d-none d-lg-block ms-auto me-2" v-if=" showMobileBtns ">
+				<div class="d-none d-lg-block ms-auto me-2" v-if="showMobileBtns">
 					<template v-for="  vals, key   in   responsiveSizes  ">
-						<button class="btn" :class=" scaleHTMLPreview == key ? 'btn-outline-primary' : '' "
-							:disabled=" scaleHTMLPreview == key " :title=" 'Switch to ' + key + ' view' "
-							v-on:click=" scaleHTMLPreview = key ">
-							<i class="bi" :class=" 'bi-' + key "></i>
+						<button class="btn" :class="scaleHTMLPreview == key ? 'btn-outline-primary' : ''"
+							:disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
+							v-on:click=" scaleHTMLPreview = key">
+							<i class="bi" :class="'bi-' + key"></i>
 						</button>
 					</template>
 				</div>
@@ -301,31 +322,31 @@ export default {
 		</nav>
 
 		<div class="tab-content mb-5" id="nav-tabContent">
-			<div v-if=" message.HTML != '' " class="tab-pane fade show" id="nav-html" role="tabpanel"
+			<div v-if="message.HTML != ''" class="tab-pane fade show" id="nav-html" role="tabpanel"
 				aria-labelledby="nav-html-tab" tabindex="0">
-				<div id="responsive-view" :class=" scaleHTMLPreview " :style=" responsiveSizes[scaleHTMLPreview] ">
-					<iframe target-blank="" class="tab-pane d-block" id="preview-html" :srcdoc=" message.HTML "
-						v-on:load=" resizeIframe " seamless frameborder="0" style="width: 100%; height: 100%;">
+				<div id="responsive-view" :class="scaleHTMLPreview" :style="responsiveSizes[scaleHTMLPreview]">
+					<iframe target-blank="" class="tab-pane d-block" id="preview-html" :srcdoc="message.HTML"
+						v-on:load="resizeIframe" seamless frameborder="0" style="width: 100%; height: 100%;">
 					</iframe>
 				</div>
-				<Attachments v-if=" allAttachments(message).length " :message=" message "
-					:attachments=" allAttachments(message) "></Attachments>
+				<Attachments v-if="allAttachments(message).length" :message="message"
+					:attachments="allAttachments(message)"></Attachments>
 			</div>
 			<div class="tab-pane fade" id="nav-html-source" role="tabpanel" aria-labelledby="nav-html-source-tab"
-				tabindex="0" v-if=" message.HTML ">
+				tabindex="0" v-if="message.HTML">
 				<pre><code class="language-html">{{ message.HTML }}</code></pre>
 			</div>
 			<div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab" tabindex="0"
-				:class=" message.HTML == '' ? 'show' : '' ">
-				<div class="text-view">{{ message.Text }}</div>
-				<Attachments v-if=" allAttachments(message).length " :message=" message "
-					:attachments=" allAttachments(message) "></Attachments>
+				:class="message.HTML == '' ? 'show' : ''">
+				<div class="text-view" v-html="textToHTML(message.Text)"></div>
+				<Attachments v-if="allAttachments(message).length" :message="message"
+					:attachments="allAttachments(message)"></Attachments>
 			</div>
 			<div class="tab-pane fade" id="nav-headers" role="tabpanel" aria-labelledby="nav-headers-tab" tabindex="0">
-				<Headers v-if=" loadHeaders " :message=" message "></Headers>
+				<Headers v-if="loadHeaders" :message="message"></Headers>
 			</div>
 			<div class="tab-pane fade" id="nav-raw" role="tabpanel" aria-labelledby="nav-raw-tab" tabindex="0">
-				<iframe v-if=" srcURI " :src=" srcURI " v-on:load=" resizeIframe " seamless frameborder="0"
+				<iframe v-if="srcURI" :src="srcURI" v-on:load="resizeIframe" seamless frameborder="0"
 					style="width: 100%; height: 300px;" id="message-src"></iframe>
 			</div>
 		</div>