Prechádzať zdrojové kódy

web: add subject to shared paste mailto link

Fixes #928. The mailto: link built by the "Email" share button had no
subject param, so the resulting email opened with a blank subject.

Reuses the existing "Encrypted note on %s" translated string (already
used for the social media preview title) rather than adding new
translations, so no i18n files need updating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
vatsanbalaji-ossagent 4 dní pred
rodič
commit
3a58c7e228
2 zmenil súbory, kde vykonal 107 pridanie a 7 odobranie
  1. 18 4
      js/privatebin.js
  2. 89 3
      js/test/emailTemplateTest.js

+ 18 - 4
js/privatebin.js

@@ -4163,16 +4163,30 @@ window.PrivateBin = (function () {
             return emailBody;
         }
 
+        /**
+         * Template Email subject.
+         *
+         * Reuses the same translated string as the page's social media
+         * preview title, so no new translations are required.
+         *
+         * @name   TopNav.templateEmailSubject
+         * @private
+         */
+        function templateEmailSubject() {
+            return I18n._('Encrypted note on %s', document.title);
+        }
+
         /**
          * Trigger Email send.
          *
          * @name   TopNav.triggerEmailSend
          * @private
+         * @param {string} subject
          * @param {string} emailBody
          */
-        function triggerEmailSend(emailBody) {
+        function triggerEmailSend(subject, emailBody) {
             window.open(
-                `mailto:?body=${encodeURIComponent(emailBody)}`,
+                `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(emailBody)}`,
                 '_self',
                 'noopener, noreferrer'
             );
@@ -4214,7 +4228,7 @@ window.PrivateBin = (function () {
                     if (bootstrap5EmailConfirmModal) {
                         bootstrap5EmailConfirmModal.hide();
                     }
-                    triggerEmailSend(emailBody);
+                    triggerEmailSend(templateEmailSubject(), emailBody);
                 }
 
                 emailconfirmmodal.addEventListener('shown.bs.modal', () => {
@@ -4232,7 +4246,7 @@ window.PrivateBin = (function () {
                     bootstrap5EmailConfirmModal.show();
                 }
             } else {
-                triggerEmailSend(templateEmailBody(null, isBurnafterreading));
+                triggerEmailSend(templateEmailSubject(), templateEmailBody(null, isBurnafterreading));
             }
         }
 

+ 89 - 3
js/test/emailTemplateTest.js

@@ -81,10 +81,20 @@ function makeWindowOpenMock() {
 }
 
 
-// Extract and decode the body from a "mailto:?body=..." URL.
+// Extract and decode the body from a "mailto:?subject=...&body=..." URL.
 function extractMailtoBody(mailtoUrl) {
-    assert.match(mailtoUrl, /^mailto:\?body=/, 'expected a mailto:?body= URL');
-    return decodeURIComponent(mailtoUrl.replace(/^mailto:\?body=/, ''));
+    assert.match(mailtoUrl, /^mailto:\?/, 'expected a mailto: URL');
+    const match = mailtoUrl.match(/[?&]body=([^&]*)/);
+    assert.ok(match, 'expected the mailto: URL to have a body= parameter');
+    return decodeURIComponent(match[1]);
+}
+
+// Extract and decode the subject from a "mailto:?subject=...&body=..." URL.
+function extractMailtoSubject(mailtoUrl) {
+    assert.match(mailtoUrl, /^mailto:\?/, 'expected a mailto: URL');
+    const match = mailtoUrl.match(/[?&]subject=([^&]*)/);
+    assert.ok(match, 'expected the mailto: URL to have a subject= parameter');
+    return decodeURIComponent(match[1]);
 }
 
 describe('Email - mail body content (short URL vs. fallback)', function () {
@@ -166,3 +176,79 @@ describe('Email - mail body content (short URL vs. fallback)', function () {
         }
     });
 });
+
+describe('Email - mail subject', function () {
+    beforeEach(function () {
+        cleanup(); // provided by common
+    });
+
+    it('Includes a non-empty subject naming the instance, with no expiration confirmation step', function () {
+        buildEmailDomNoShortUrl();
+        // buildEmailDomNoShortUrl() replaces documentElement.innerHTML (dropping <title>),
+        // so document.title must be (re-)set after calling it, not before.
+        document.title = 'My PrivateBin Instance';
+        PrivateBin.TopNav.init();
+        PrivateBin.TopNav.showEmailButton(0);
+
+        const { getUrl, restore } = makeWindowOpenMock();
+        try {
+            document.getElementById('emaillink').click();
+
+            const openedUrl = getUrl();
+            assert.ok(openedUrl, 'window.open should have been called');
+
+            const subject = extractMailtoSubject(openedUrl);
+            assert.match(subject, /My PrivateBin Instance/, 'subject should name the instance');
+        } finally {
+            restore();
+        }
+    });
+
+    it('Includes the same subject after the expiration confirmation step', function () {
+        buildEmailDomWithShortUrl();
+        document.title = 'My PrivateBin Instance';
+        PrivateBin.TopNav.init();
+        // a non-zero remaining time routes through the timezone confirmation modal
+        PrivateBin.TopNav.showEmailButton(3600);
+
+        const { getUrl, restore } = makeWindowOpenMock();
+        try {
+            document.getElementById('emaillink').click();
+            document.getElementById('emailconfirm-timezone-current').click();
+
+            const openedUrl = getUrl();
+            assert.ok(openedUrl, 'window.open should have been called');
+
+            const subject = extractMailtoSubject(openedUrl);
+            assert.match(subject, /My PrivateBin Instance/, 'subject should name the instance');
+        } finally {
+            restore();
+        }
+    });
+
+    it('Percent-encodes the subject so a space-containing instance name does not break the mailto URL', function () {
+        buildEmailDomNoShortUrl();
+        document.title = 'My Cool Instance';
+        PrivateBin.TopNav.init();
+        PrivateBin.TopNav.showEmailButton(0);
+
+        const { getUrl, restore } = makeWindowOpenMock();
+        try {
+            document.getElementById('emaillink').click();
+
+            const openedUrl = getUrl();
+            const rawSubjectParam = openedUrl.split('&body=')[0];
+            assert.strictEqual(
+                rawSubjectParam,
+                `mailto:?subject=${encodeURIComponent('Encrypted note on My Cool Instance')}`,
+                'raw subject parameter should be exactly the encoded translated string'
+            );
+            assert.doesNotMatch(rawSubjectParam, / /, 'raw mailto URL must not contain a literal space');
+
+            const subject = extractMailtoSubject(openedUrl);
+            assert.strictEqual(subject, 'Encrypted note on My Cool Instance', 'decoded subject should round-trip exactly');
+        } finally {
+            restore();
+        }
+    });
+});