diff --git a/firefox/manifest.merge.json b/firefox/manifest.merge.json
index 25009589cec73d4986636baf17591e0f45f93b5b..14fcf6761bcc29132c445b916d6cf1459262d0cd 100644
--- a/firefox/manifest.merge.json
+++ b/firefox/manifest.merge.json
@@ -1,6 +1,7 @@
 {
     "background": {
         "scripts": [
+	    "patterns.js",
 	    "links.js",
 	    "menu.js"
         ]
@@ -16,6 +17,7 @@
 	    ],
 	    "js": [
 		"mutation.js",
+		"patterns.js",
 		"links.js",
 		"popup.js",
 		"content.js"
diff --git a/shared/links.js b/shared/links.js
index 3c817b123845a990de6a0622345e21486824fd3d..9d9624a60a1e9a72e55b4f112fdaf806a5bc796b 100644
--- a/shared/links.js
+++ b/shared/links.js
@@ -22,25 +22,6 @@
 // Shared code
 
 
-/**
- * List of regexps that match safe links. The original URL must be
- * collected in match group 1.
- */
-const regexpList = [
-    'https?://[^.]+[.]safelinks[.]protection[.]outlook[.]com/[?]url=([^&]+)&.*',
-    'https?://linkprotect[.]cudasvc[.]com/url[?]a=([^&]+)&.*'
-];
-
-
-/**
- * Concatenated regexp for all safe links types.
- */
-const safelinksRegexp = new RegExp(
-    '(?:' + regexpList.map((string) => '(?:' + string + ')').join('|') + ')',
-    'gi'
-);
-
-
 /**
  * The ID for the popup element that is added to the HTML document.
  */
diff --git a/shared/menu.js b/shared/menu.js
index 22d58cd64c706c5cfc885334766551ffbe6ceabb..8099e43839d2fbab84f8ef26e549989727de0510 100644
--- a/shared/menu.js
+++ b/shared/menu.js
@@ -54,7 +54,7 @@ browser.menus.create({
     title: browser.i18n.getMessage("copyLinkMenuTitle"),
     contexts: ["link"],
     visible: true,
-    targetUrlPatterns: ["*://*.safelinks.protection.outlook.com/*"],
+    targetUrlPatterns: computeSafelinksMatchPatterns()
 });
 
 browser.menus.onClicked.addListener((info, tab) => {
diff --git a/shared/patterns.js b/shared/patterns.js
new file mode 100644
index 0000000000000000000000000000000000000000..a52e3c5364c27582f7bbc6c7c2e501d46acb880f
--- /dev/null
+++ b/shared/patterns.js
@@ -0,0 +1,54 @@
+// Safe Links Cleaner
+// Copyright 2021 David Byers <david.byers@liu.se>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+// Shared code
+
+
+
+const safelinksPatterns = [
+    {
+	host: '*.safelinks.protection.outlook.com',
+	path: '[?]url=([^&]+).*'
+    },
+    {
+	host: 'linkprotect.cudasvc.com',
+	path: 'url[?]a=([^&]+).*'
+    }
+];
+
+function computeSafelinksMatchPatterns() {
+    return safelinksPatterns.map(({host, path}) => '*://' + host + '/*');
+}
+
+function escapeHostname(host) {
+    return host.replace('.', '[.]').replace('*', '[^/]*')
+}
+
+function computeSafelinksRegexp() {
+    return new RegExp('(?:' +
+		      safelinksPatterns.map(
+			  ({host, path}) =>
+			      `(?:https?://${escapeHostname(host)}/${path})`).join('|') +
+		      ')',
+	'gi');
+}
+
+const safelinksRegexp = computeSafelinksRegexp()
diff --git a/thunderbird/background.js b/thunderbird/background.js
index 28df119348754275ab22ec40920ad352d2e9eefc..31830d63f6ad6327ea0fd28caf09735dd8004ff5 100644
--- a/thunderbird/background.js
+++ b/thunderbird/background.js
@@ -28,6 +28,7 @@ browser.composeScripts.register({
     ],
     js: [
 	{file: "mutation.js"},
+	{file: "patterns.js"},
 	{file: "links.js"},
 	{file: "popup.js"},
 	{file: "compose.js"}
@@ -40,6 +41,7 @@ browser.messageDisplayScripts.register({
     ],
     js: [
 	{file: "mutation.js"},
+	{file: "patterns.js"},
 	{file: "links.js"},
 	{file: "popup.js"},
 	{file: "display.js"},
diff --git a/thunderbird/manifest.merge.json b/thunderbird/manifest.merge.json
index 2b4937ad753f1abb8a1ed366f61d8a5ed705b67c..343d2c2cf152482ee6a1ab95850ec059e2bd2991 100644
--- a/thunderbird/manifest.merge.json
+++ b/thunderbird/manifest.merge.json
@@ -7,6 +7,7 @@
     },
     "background": {
         "scripts": [
+	    "patterns.js",
 	    "links.js",
 	    "menu.js",
             "background.js"