feat: ✨ Add Chrome extension
Tim Izzo tim@5ika.ch
Thu, 25 Jun 2026 13:33:31 +0200
16 files changed,
356 insertions(+),
82 deletions(-)
A
README.md
@@ -0,0 +1,6 @@
+# Mailcatcher Viewer + +Une extension de navigateur simple pour visualiser les emails capturés par une instance de [Mailcatcher](https://github.com/mailcatcher/mailcatcher). + +- [Extension Firefox](./firefox/) +- [Extension Chrome](./chrome/)
A
chrome/README.md
@@ -0,0 +1,10 @@
+# Mailcatcher viewer - Chrome + +Firefox extension to view emails sent by Mailcatcher quickly. + +## Installation + +1. In Chrome, go to `Extensions` then `Manage extensions` +2. Enable `Developer mode` on top-right corner +3. Click on ` Load unpacked` +4. Select the `manifest.json` file in this repository from your computer
A
chrome/manifest.json
@@ -0,0 +1,18 @@
+{ + "manifest_version": 3, + "name": "Mailcatcher Viewer", + "version": "1.0", + "description": "Affiche les emails depuis une instance Mailcatcher", + "permissions": ["storage", "activeTab"], + "host_permissions": ["http://*/*", "https://*/*"], + "action": { + "default_popup": "popup.html", + "default_icon": "icons/48.png" + }, + "icons": { + "16": "icons/16.png", + "32": "icons/32.png", + "48": "icons/48.png", + "128": "icons/128.png" + } +}
A
chrome/popup.html
@@ -0,0 +1,105 @@
+<!doctype html> +<html> + <head> + <meta charset="utf-8" /> + <title>Mailcatcher Viewer</title> + <style> + body { + width: 400px; + padding: 0.5rem; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; + color: #333; + } + h1 { + font-size: 18px; + color: #2c3e50; + margin-top: 0; + margin-bottom: 15px; + border-bottom: 2px solid #3498db; + padding-bottom: 5px; + } + #config { + margin-top: 15px; + padding: 10px; + background-color: #fff; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #2c3e50; + } + input[type="text"] { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + margin-bottom: 10px; + } + button { + background-color: #3498db; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.3s; + font-size: 0.8rem; + } + button:hover { + background-color: #2980b9; + } + .email-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid #e0e0e0; + margin-top: 10px; + border-radius: 5px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + .email-item { + padding: 12px 15px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.2s; + } + .email-item:hover { + background-color: #e8f4fc; + } + .email-item:last-child { + border-bottom: none; + } + .email-title { + font-weight: bold; + } + .email-sender { + color: #2c3e50; + margin-bottom: 3px; + } + .email-meta { + font-size: 12px; + color: #7f8c8d; + } + </style> + </head> + <body> + <button id="refresh-emails">Rafraîchir les emails</button> + <div class="email-list" id="email-list"></div> + <div id="config"> + <label for="mailcatcher-url">URL de Mailcatcher :</label> + <input + type="text" + id="mailcatcher-url" + placeholder="http://localhost:1080" + /> + <button id="save-url">Enregistrer</button> + </div> + <script src="popup.js"></script> + </body> +</html>
A
chrome/popup.js
@@ -0,0 +1,62 @@
+// Sauvegarde l'URL de Mailcatcher +document.getElementById("save-url").addEventListener("click", () => { + const url = document.getElementById("mailcatcher-url").value; + if (url) { + browser.storage.local.set({ mailcatcherUrl: url }); + alert("URL enregistrée !"); + } +}); + +// Charge l'URL sauvegardée au démarrage +browser.storage.local.get("mailcatcherUrl").then((data) => { + if (data.mailcatcherUrl) { + document.getElementById("mailcatcher-url").value = data.mailcatcherUrl; + fetchEmails(data.mailcatcherUrl); + } +}); + +// Rafraîchit la liste des emails +document.getElementById("refresh-emails").addEventListener("click", () => { + browser.storage.local.get("mailcatcherUrl").then((data) => { + if (data.mailcatcherUrl) { + fetchEmails(data.mailcatcherUrl); + } else { + alert("Veuillez configurer l'URL de Mailcatcher."); + } + }); +}); + +// Récupère les emails depuis Mailcatcher +function fetchEmails(baseUrl) { + const emailList = document.getElementById("email-list"); + emailList.innerHTML = "<p>Chargement...</p>"; + + fetch(`${baseUrl}/messages`) + .then((response) => response.json()) + .then((emails) => { + emailList.innerHTML = ""; + if (emails.length === 0) { + emailList.innerHTML = "<p>Aucun email trouvé.</p>"; + return; + } + emails.reverse().forEach((email) => { + const item = document.createElement("div"); + item.className = "email-item"; + item.innerHTML = ` + <div class="email-title">${email.subject}</div> + <div class="email-sender">${cleanAddress(email.sender)}</div> + <div class="email-meta">À : ${cleanAddress(email.recipients.join(", "))} • ${new Date(email.created_at).toLocaleString()}</div> + `; + item.addEventListener( + "click", + () => (window.location.href = `${baseUrl}/messages/${email.id}.html`), + ); + emailList.appendChild(item); + }); + }) + .catch((error) => { + emailList.innerHTML = `<p>Erreur : ${error.message}</p>`; + }); +} + +const cleanAddress = (rawAddress) => rawAddress.replace(/[<>"']/g, "");
A
firefox/README.md
@@ -0,0 +1,14 @@
+# Mailcatcher viewer - Firefox + +Firefox extension to view emails sent by Mailcatcher quickly. + +## Installation + +1. Go to https://addons.mozilla.org/fr/firefox/addon/mailcatcher-viewer/ +2. Click on `Add to Firefox` + +## Installation (dev mode) + +1. In Firefox, go to `about:debugging` +2. Click on ` Load Temporary Add-on` +3. Select the `manifest.json` file in this repository from your computer
A
firefox/mail.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M21 8L17.4392 9.97822C15.454 11.0811 14.4614 11.6326 13.4102 11.8488C12.4798 12.0401 11.5202 12.0401 10.5898 11.8488C9.53864 11.6326 8.54603 11.0811 6.5608 9.97822L3 8M6.2 19H17.8C18.9201 19 19.4802 19 19.908 18.782C20.2843 18.5903 20.5903 18.2843 20.782 17.908C21 17.4802 21 16.9201 21 15.8V8.2C21 7.0799 21 6.51984 20.782 6.09202C20.5903 5.71569 20.2843 5.40973 19.908 5.21799C19.4802 5 18.9201 5 17.8 5H6.2C5.0799 5 4.51984 5 4.09202 5.21799C3.71569 5.40973 3.40973 5.71569 3.21799 6.09202C3 6.51984 3 7.07989 3 8.2V15.8C3 16.9201 3 17.4802 3.21799 17.908C3.40973 18.2843 3.71569 18.5903 4.09202 18.782C4.51984 19 5.07989 19 6.2 19Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</svg>
A
firefox/manifest.json
@@ -0,0 +1,24 @@
+{ + "manifest_version": 2, + "name": "Mailcatcher Viewer", + "version": "1.0", + "description": "Affiche les emails depuis une instance Mailcatcher", + "permissions": ["storage", "http://*/", "https://*/", "activeTab"], + "browser_action": { + "default_popup": "popup.html", + "default_icon": "mail.svg" + }, + "icons": { + "48": "mail.svg", + "96": "mail.svg" + }, + "browser_specific_settings": { + "gecko": { + "id": "@5ika-mailcatcher-viewer", + "data_collection_permissions": { + "required": ["none"], + "optional": [] + } + } + } +}
A
firefox/popup.html
@@ -0,0 +1,105 @@
+<!doctype html> +<html> + <head> + <meta charset="utf-8" /> + <title>Mailcatcher Viewer</title> + <style> + body { + width: 400px; + padding: 0.5rem; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; + color: #333; + } + h1 { + font-size: 18px; + color: #2c3e50; + margin-top: 0; + margin-bottom: 15px; + border-bottom: 2px solid #3498db; + padding-bottom: 5px; + } + #config { + margin-top: 15px; + padding: 10px; + background-color: #fff; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #2c3e50; + } + input[type="text"] { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + margin-bottom: 10px; + } + button { + background-color: #3498db; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + transition: background-color 0.3s; + font-size: 0.8rem; + } + button:hover { + background-color: #2980b9; + } + .email-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid #e0e0e0; + margin-top: 10px; + border-radius: 5px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + .email-item { + padding: 12px 15px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background-color 0.2s; + } + .email-item:hover { + background-color: #e8f4fc; + } + .email-item:last-child { + border-bottom: none; + } + .email-title { + font-weight: bold; + } + .email-sender { + color: #2c3e50; + margin-bottom: 3px; + } + .email-meta { + font-size: 12px; + color: #7f8c8d; + } + </style> + </head> + <body> + <button id="refresh-emails">Rafraîchir les emails</button> + <div class="email-list" id="email-list"></div> + <div id="config"> + <label for="mailcatcher-url">URL de Mailcatcher :</label> + <input + type="text" + id="mailcatcher-url" + placeholder="http://localhost:1080" + /> + <button id="save-url">Enregistrer</button> + </div> + <script src="popup.js"></script> + </body> +</html>
D
manifest.json
@@ -1,15 +0,0 @@
-{ - "manifest_version": 2, - "name": "Mailcatcher Viewer", - "version": "1.0", - "description": "Affiche les emails depuis une instance Mailcatcher", - "permissions": ["storage", "http://*/", "https://*/", "activeTab"], - "browser_action": { - "default_popup": "popup.html", - "default_icon": "icons/icon-48.png" - }, - "icons": { - "48": "icons/icon-48.png", - "96": "icons/icon-96.png" - } -}
D
popup.html
@@ -1,49 +0,0 @@
-<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8" /> - <title>Mailcatcher Viewer</title> - <style> - body { - width: 400px; - padding: 10px; - font-family: Arial, sans-serif; - } - .email-list { - max-height: 300px; - overflow-y: auto; - border: 1px solid #ccc; - margin-top: 10px; - } - .email-item { - padding: 8px; - border-bottom: 1px solid #eee; - cursor: pointer; - } - .email-item:hover { - background-color: #f5f5f5; - } - #config { - margin-bottom: 10px; - } - button { - margin-top: 5px; - padding: 5px 10px; - } - </style> - </head> - <body> - <div id="config"> - <label for="mailcatcher-url">URL de Mailcatcher :</label> - <input - type="text" - id="mailcatcher-url" - placeholder="http://localhost:1080" - /> - <button id="save-url">Enregistrer</button> - </div> - <button id="refresh-emails">Rafraîchir</button> - <div class="email-list" id="email-list"></div> - <script src="popup.js"></script> - </body> -</html>
M
popup.js
→
firefox/popup.js
@@ -39,15 +39,17 @@ if (emails.length === 0) {
emailList.innerHTML = "<p>Aucun email trouvé.</p>"; return; } - emails.forEach((email) => { + emails.reverse().forEach((email) => { const item = document.createElement("div"); item.className = "email-item"; item.innerHTML = ` - <strong>${email.sender}</strong> → ${email.recipients.join(", ")} - <br><small>${new Date(email.sent_at).toLocaleString()}</small> + <div class="email-title">${email.subject}</div> + <div class="email-sender">${cleanAddress(email.sender)}</div> + <div class="email-meta">À : ${cleanAddress(email.recipients.join(", "))} • ${new Date(email.created_at).toLocaleString()}</div> `; - item.addEventListener("click", () => - showEmailDetails(baseUrl, email.id), + item.addEventListener( + "click", + () => (window.location.href = `${baseUrl}/messages/${email.id}.html`), ); emailList.appendChild(item); });@@ -57,16 +59,4 @@ emailList.innerHTML = `<p>Erreur : ${error.message}</p>`;
}); } -// Affiche les détails d'un email -function showEmailDetails(baseUrl, emailId) { - fetch(`${baseUrl}/messages/${emailId}.json`) - .then((response) => response.json()) - .then((email) => { - alert( - `De : ${email.sender}\nÀ : ${email.recipients}\nSujet : ${email.subject}\n\n${email.text_body || email.html_body}`, - ); - }) - .catch((error) => { - alert(`Erreur : ${error.message}`); - }); -} +const cleanAddress = (rawAddress) => rawAddress.replace(/[<>"']/g, "");