فهرست منبع

Merge branch 'Haocen-398'

El RIDO 6 سال پیش
والد
کامیت
ffe26a8841
7فایلهای تغییر یافته به همراه318 افزوده شده و 9 حذف شده
  1. 1 0
      CHANGELOG.md
  2. 25 0
      css/bootstrap/privatebin.css
  3. BIN
      img/icon_email.png
  4. 255 4
      js/privatebin.js
  5. 1 1
      js/test/TopNav.js
  6. 34 3
      tpl/bootstrap.php
  7. 2 1
      tpl/page.php

+ 1 - 0
CHANGELOG.md

@@ -1,6 +1,7 @@
 # PrivateBin version history
 # PrivateBin version history
 
 
   * **1.4 (not yet released)**
   * **1.4 (not yet released)**
+    * ADDED: Option to send a mail with the link, when creating a paste (#398)
     * FIXED: Password disabling option (#527)
     * FIXED: Password disabling option (#527)
   * **1.3.1 (2019-09-22)**
   * **1.3.1 (2019-09-22)**
     * ADDED: Translation for Bulgarian (#455)
     * ADDED: Translation for Bulgarian (#455)

+ 25 - 0
css/bootstrap/privatebin.css

@@ -180,3 +180,28 @@ li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 {
 		padding-right: 4ch;
 		padding-right: 4ch;
 	}
 	}
 }
 }
+
+.modal-dialog {
+	margin: auto !important;
+}
+
+/* makeup for the original margin on modal-dialog */
+@media (min-width: 768px) {
+	.modal-content {
+		margin: 30px 0;
+	}
+}
+
+.modal-content {
+	margin: 10px;
+}
+
+.modal-body {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.modal .modal-content button {
+	margin: 0.5em 0;
+}

BIN
img/icon_email.png


+ 255 - 4
js/privatebin.js

@@ -403,6 +403,45 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             baseUri = null;
             baseUri = null;
         };
         };
 
 
+        /**
+         * calculate expiration date given initial date and expiration period
+         * 
+         * @name   Helper.calculateExpirationDate
+         * @function
+         * @param  {Date} initialDate - may not be empty
+         * @param  {string|number} expirationDisplayStringOrSecondsToExpire - may not be empty
+         * @return {Date}
+         */
+        me.calculateExpirationDate = function(initialDate, expirationDisplayStringOrSecondsToExpire) {
+            let expirationDate = new Date(initialDate);
+
+            const expirationDisplayStringToSecondsDict = {
+                '5min': 300,
+                '10min': 600,
+                '1hour': 3500,
+                '1day': 86400,
+                '1week': 604800,
+                '1month': 2592000,
+                '1year': 31536000,
+                'never': 0
+            };
+
+            let secondsToExpiration = expirationDisplayStringOrSecondsToExpire;
+            if (typeof expirationDisplayStringOrSecondsToExpire === 'string') {
+                secondsToExpiration = expirationDisplayStringToSecondsDict[expirationDisplayStringOrSecondsToExpire];
+            }
+            
+            if (typeof secondsToExpiration !== 'number') {
+                throw new Error('Cannot calculate expiration date.');
+            }
+            if (secondsToExpiration === 0) {
+                return null;
+            }
+
+            expirationDate = expirationDate.setUTCSeconds(expirationDate.getUTCSeconds() + secondsToExpiration);
+            return expirationDate;
+        }
+
         return me;
         return me;
     })();
     })();
 
 
@@ -1822,7 +1861,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                     `${$shortenButton.data('shortener')}${encodeURIComponent($pasteUrl.attr('href'))}`,
                     `${$shortenButton.data('shortener')}${encodeURIComponent($pasteUrl.attr('href'))}`,
                     '_blank',
                     '_blank',
                     'noopener, noreferrer'
                     'noopener, noreferrer'
-                )
+                );
             });
             });
         }
         }
 
 
@@ -3332,9 +3371,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             $passwordInput,
             $passwordInput,
             $rawTextButton,
             $rawTextButton,
             $qrCodeLink,
             $qrCodeLink,
+            $emailLink,
             $sendButton,
             $sendButton,
             $retryButton,
             $retryButton,
-            pasteExpiration = '1week',
+            pasteExpiration = null,
             retryButtonCallback;
             retryButtonCallback;
 
 
         /**
         /**
@@ -3542,6 +3582,133 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             $('#qrcode-display').html(qrCanvas);
             $('#qrcode-display').html(qrCanvas);
         }
         }
 
 
+        /**
+         * Template Email body.
+         * 
+         * @name   TopNav.templateEmailBody
+         * @private 
+         * @param {string} expirationDateString 
+         * @param {bool} isBurnafterreading 
+         */
+        function templateEmailBody(expirationDateString, isBurnafterreading)
+        {
+            const EOL = '\n';
+            const BULLET = '  - ';
+            let emailBody = '';
+            if (expirationDateString !== null || isBurnafterreading) {
+                emailBody += I18n._('Notice:');
+                emailBody += EOL;
+
+                if (expirationDateString !== null) {
+                    emailBody += EOL;
+                    emailBody += BULLET;
+                    emailBody += I18n._(
+                        'This link will expire after %s.',
+                        expirationDateString
+                    );
+                }
+                if (isBurnafterreading) {
+                    emailBody += EOL;
+                    emailBody += BULLET;
+                    emailBody += I18n._(
+                        'This link can only be accessed once, do not use back or refresh button in your browser.'
+                    );
+                }
+
+                emailBody += EOL;
+                emailBody += EOL;
+            }
+            emailBody += I18n._('Link:');
+            emailBody += EOL;
+            emailBody += `${window.location.href}`;
+            return emailBody;
+        }
+
+        /**
+         * Trigger Email send.
+         * 
+         * @name   TopNav.triggerEmailSend
+         * @private 
+         * @param {string} emailBody 
+         */
+        function triggerEmailSend(emailBody)
+        {
+            window.open(
+                `mailto:?body=${encodeURIComponent(emailBody)}`,
+                '_self',
+                'noopener, noreferrer'
+            );
+        }
+
+        /**
+         * Send Email with current paste (URL).
+         *
+         * @name   TopNav.sendEmail
+         * @private
+         * @function
+         * @param  {Date|null} expirationDate date of expiration
+         * @param  {bool} isBurnafterreading whether it is burn after reading
+         */
+        function sendEmail(expirationDate, isBurnafterreading)
+        {
+            const expirationDateRoundedToSecond = new Date(expirationDate);
+
+            // round down at least 30 seconds to make up for the delay of request
+            expirationDateRoundedToSecond.setUTCSeconds(
+                expirationDateRoundedToSecond.getUTCSeconds() - 30
+            );
+            expirationDateRoundedToSecond.setUTCSeconds(0);
+
+            const $emailconfirmmodal = $('#emailconfirmmodal');
+            if ($emailconfirmmodal.length > 0) {
+                if (expirationDate !== null) {
+                    $emailconfirmmodal.find('#emailconfirm-display').text(
+                        I18n._('Recipient may become aware of your timezone, convert time to UTC?')
+                    );
+                    const $emailconfirmTimezoneCurrent = $emailconfirmmodal.find('#emailconfirm-timezone-current');
+                    const $emailconfirmTimezoneUtc = $emailconfirmmodal.find('#emailconfirm-timezone-utc');
+                    $emailconfirmTimezoneCurrent.off('click.sendEmailCurrentTimezone');
+                    $emailconfirmTimezoneCurrent.on('click.sendEmailCurrentTimezone', function(expirationDateRoundedToSecond, isBurnafterreading) {
+                        return function() {
+                            const emailBody = templateEmailBody(expirationDateRoundedToSecond.toLocaleString(), isBurnafterreading);
+                            $emailconfirmmodal.modal('hide');
+                            triggerEmailSend(emailBody);
+                        };
+                    } (expirationDateRoundedToSecond, isBurnafterreading));
+                    $emailconfirmTimezoneUtc.off('click.sendEmailUtcTimezone');
+                    $emailconfirmTimezoneUtc.on('click.sendEmailUtcTimezone', function(expirationDateRoundedToSecond, isBurnafterreading) {
+                        return function() {
+                            const emailBody = templateEmailBody(expirationDateRoundedToSecond.toLocaleString(
+                                undefined,
+                                // we don't use Date.prototype.toUTCString() because we would like to avoid GMT
+                                { timeZone: 'UTC', dateStyle: 'long', timeStyle: 'long' }
+                            ), isBurnafterreading);
+                            $emailconfirmmodal.modal('hide');
+                            triggerEmailSend(emailBody);
+                        };
+                    } (expirationDateRoundedToSecond, isBurnafterreading));
+                    $emailconfirmmodal.modal('show');
+                } else {
+                    triggerEmailSend(templateEmailBody(null, isBurnafterreading));
+                }
+            } else {
+                let emailBody = '';
+                if (expirationDate !== null) {
+                    const expirationDateString = window.confirm(
+                        I18n._('Recipient may become aware of your timezone, convert time to UTC?')
+                    ) ? expirationDateRoundedToSecond.toLocaleString(
+                        undefined,
+                        // we don't use Date.prototype.toUTCString() because we would like to avoid GMT
+                        { timeZone: 'UTC', dateStyle: 'long', timeStyle: 'long' }
+                    ) : expirationDateRoundedToSecond.toLocaleString();
+                    emailBody = templateEmailBody(expirationDateString, isBurnafterreading);
+                } else {
+                    emailBody = templateEmailBody(null, isBurnafterreading);
+                }
+                triggerEmailSend(emailBody);
+            }
+        }
+
         /**
         /**
          * Shows all navigation elements for viewing an existing paste
          * Shows all navigation elements for viewing an existing paste
          *
          *
@@ -3578,6 +3745,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             $newButton.addClass('hidden');
             $newButton.addClass('hidden');
             $rawTextButton.addClass('hidden');
             $rawTextButton.addClass('hidden');
             $qrCodeLink.addClass('hidden');
             $qrCodeLink.addClass('hidden');
+            me.hideEmailButton();
 
 
             viewButtonsDisplayed = false;
             viewButtonsDisplayed = false;
         };
         };
@@ -3675,6 +3843,50 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             $retryButton.addClass('hidden');
             $retryButton.addClass('hidden');
         }
         }
 
 
+        /**
+         * show the "email" button
+         * 
+         * @name   TopNav.showEmailbutton
+         * @function
+         * @param {int|undefined} optionalRemainingTimeInSeconds
+         */
+        me.showEmailButton = function(optionalRemainingTimeInSeconds)
+        {
+            try {
+                // we cache expiration date in closure to avoid inaccurate expiration datetime
+                const expirationDate = Helper.calculateExpirationDate(
+                    new Date(),
+                    typeof optionalRemainingTimeInSeconds === 'number' ? optionalRemainingTimeInSeconds : TopNav.getExpiration()
+                );
+                const isBurnafterreading = TopNav.getBurnAfterReading();
+
+                $emailLink.removeClass('hidden');
+                $emailLink.off('click.sendEmail');
+                $emailLink.on('click.sendEmail', function(expirationDate, isBurnafterreading) {
+                    return function() {
+                        sendEmail(expirationDate, isBurnafterreading);
+                    };
+                } (expirationDate, isBurnafterreading));
+            } catch (error) {
+                console.error(error);
+                Alert.showError(
+                    I18n._('Cannot calculate expiration date.')
+                );
+            }
+        }
+
+        /**
+         * hide the "email" button
+         * 
+         * @name   TopNav.hideEmailButton
+         * @function
+         */
+        me.hideEmailButton = function()
+        {
+            $emailLink.addClass('hidden');
+            $emailLink.off('click.sendEmail');
+        }
+
         /**
         /**
          * only hides the clone button
          * only hides the clone button
          *
          *
@@ -3697,6 +3909,30 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             $rawTextButton.addClass('hidden');
             $rawTextButton.addClass('hidden');
         };
         };
 
 
+        /**
+         * only hides the qr code button
+         * 
+         * @name   TopNav.hideQrCodeButton
+         * @function
+         */
+        me.hideQrCodeButton = function()
+        {
+            $qrCodeLink.addClass('hidden');
+        }
+
+        /**
+         * hide all irrelevant buttons when viewing burn after reading paste
+         * 
+         * @name   TopNav.hideBurnAfterReadingButtons
+         * @function
+         */
+        me.hideBurnAfterReadingButtons = function()
+        {
+            me.hideCloneButton();
+            me.hideQrCodeButton();
+            me.hideEmailButton();
+        }
+
         /**
         /**
          * hides the file selector in attachment
          * hides the file selector in attachment
          *
          *
@@ -3784,7 +4020,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
         /**
         /**
          * returns the state of the burn after reading checkbox
          * returns the state of the burn after reading checkbox
          *
          *
-         * @name   TopNav.getExpiration
+         * @name   TopNav.getBurnAfterReading
          * @function
          * @function
          * @return {bool}
          * @return {bool}
          */
          */
@@ -3914,6 +4150,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             $retryButton = $('#retrybutton');
             $retryButton = $('#retrybutton');
             $sendButton = $('#sendbutton');
             $sendButton = $('#sendbutton');
             $qrCodeLink = $('#qrcodelink');
             $qrCodeLink = $('#qrcodelink');
+            $emailLink = $('#emaillink');
 
 
             // bootstrap template drop down
             // bootstrap template drop down
             $('#language ul.dropdown-menu li a').click(setLanguage);
             $('#language ul.dropdown-menu li a').click(setLanguage);
@@ -4258,6 +4495,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
             history.pushState({type: 'newpaste'}, document.title, url);
             history.pushState({type: 'newpaste'}, document.title, url);
 
 
             TopNav.showViewButtons();
             TopNav.showViewButtons();
+
+            // this cannot be grouped with showViewButtons due to remaining time calculation
+            TopNav.showEmailButton();
+
             TopNav.hideRawButton();
             TopNav.hideRawButton();
             Editor.hide();
             Editor.hide();
 
 
@@ -4698,7 +4939,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
 
 
                     // discourage cloning (it cannot really be prevented)
                     // discourage cloning (it cannot really be prevented)
                     if (paste.isBurnAfterReadingEnabled()) {
                     if (paste.isBurnAfterReadingEnabled()) {
-                        TopNav.hideCloneButton();
+                        TopNav.hideBurnAfterReadingButtons();
+                    } else {
+                        // we have to pass in remaining_time here
+                        TopNav.showEmailButton(paste.getTimeToLive());
                     }
                     }
                 })
                 })
                 .catch((err) => {
                 .catch((err) => {
@@ -4914,6 +5158,13 @@ jQuery.PrivateBin = (function($, RawDeflate) {
 
 
             DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
             DOMPurify.setConfig({SAFE_FOR_JQUERY: true});
 
 
+            // center all modals
+            $('.modal').on('show.bs.modal', function(e) {
+                $(e.target).css({
+                    display: 'flex'
+                });
+            });
+
             // initialize other modules/"classes"
             // initialize other modules/"classes"
             Alert.init();
             Alert.init();
             Model.init();
             Model.init();

+ 1 - 1
js/test/TopNav.js

@@ -326,7 +326,7 @@ describe('TopNav', function () {
             'returns the currently selected expiration date',
             'returns the currently selected expiration date',
             function () {
             function () {
                 $.PrivateBin.TopNav.init();
                 $.PrivateBin.TopNav.init();
-                assert.ok($.PrivateBin.TopNav.getExpiration() === '1week');
+                assert.ok($.PrivateBin.TopNav.getExpiration() === null);
             }
             }
         );
         );
     });
     });

+ 34 - 3
tpl/bootstrap.php

@@ -72,7 +72,7 @@ endif;
 ?>
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.1.js" integrity="sha512-ddI36MdUoXp/o7yhQtr9/qj4G3oFwCRga4jCGaoUYtORg0PPmFKVKG4Ess3fIknYzxwwKMlrIL9o4NwuPTCc1Q==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.1.js" integrity="sha512-ddI36MdUoXp/o7yhQtr9/qj4G3oFwCRga4jCGaoUYtORg0PPmFKVKG4Ess3fIknYzxwwKMlrIL9o4NwuPTCc1Q==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-rgDjBvJguZa4Liw2R/0vtFNZBSBTLhjdHROUseVyl+x1aVUbo5U8x/5lewWzYxrS9dxHOtgQkEWjx5ZM31JiNg==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-60d7BURzTV858Sry9vJEMgDXEqLT/IqQnjnx/iR3QfQ0LLWZI3bbM9nb4vbS0vQPYFVOP8lXpHRnNE7QEohsDw==" crossorigin="anonymous"></script>
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
@@ -118,13 +118,41 @@ if ($QRCODE):
 					<div class="modal-body">
 					<div class="modal-body">
 						<div class="mx-auto" id="qrcode-display"></div>
 						<div class="mx-auto" id="qrcode-display"></div>
 					</div>
 					</div>
-					<button type="button" class="btn btn-primary btn-block" data-dismiss="modal"><?php echo I18n::_('Close') ?></button>
+					<div class="row">
+						<div class="btn-group col-xs-12">
+							<span class="col-xs-12">
+								<button type="button" class="btn btn-primary btn-block" data-dismiss="modal"><?php echo I18n::_('Close') ?></button>
+							</span>
+						</div>
+					</div>
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>
 <?php
 <?php
 endif;
 endif;
 ?>
 ?>
+		<div id="emailconfirmmodal" tabindex="-1" class="modal fade" aria-labelledby="emailconfirmmodalTitle" role="dialog" aria-hidden="true">
+			<div class="modal-dialog" role="document">
+				<div class="modal-content">
+					<div class="modal-body">
+						<div id="emailconfirm-display"></div>
+					</div>
+					<div class="row">
+						<div class="btn-group col-xs-12" data-toggle="buttons">
+							<span class="col-xs-12 col-md-4">
+								<button id="emailconfirm-timezone-current" type="button" class="btn btn-danger btn-block" data-dismiss="modal"><?php echo I18n::_('Use Current Timezone') ?></button>
+							</span>
+							<span class="col-xs-12 col-md-4">
+								<button id="emailconfirm-timezone-utc" type="button" class="btn btn-default btn-block" data-dismiss="modal"><?php echo I18n::_('Convert To UTC') ?></button>
+							</span>
+							<span class="col-xs-12 col-md-4">
+								<button type="button" class="btn btn-primary btn-block" data-dismiss="modal"><?php echo I18n::_('Close') ?></button>
+							</span>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
 		<nav class="navbar navbar-<?php echo $isDark ? 'inverse' : 'default'; ?> navbar-<?php echo $isCpct ? 'fixed' : 'static'; ?>-top"><?php
 		<nav class="navbar navbar-<?php echo $isDark ? 'inverse' : 'default'; ?> navbar-<?php echo $isCpct ? 'fixed' : 'static'; ?>-top"><?php
 if ($isCpct):
 if ($isCpct):
 ?><div class="container"><?php
 ?><div class="container"><?php
@@ -171,6 +199,9 @@ endif;
 						<button id="rawtextbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
 						<button id="rawtextbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
 							<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
 							<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
 						</button>
 						</button>
+						<button id="emaillink" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
+							<span class="glyphicon glyphicon-envelope" aria-hidden="true"></span> <?php echo I18n::_('Email'), PHP_EOL; ?>
+						</button>
 <?php
 <?php
 if ($QRCODE):
 if ($QRCODE):
 ?>
 ?>
@@ -559,4 +590,4 @@ if ($FILEUPLOAD):
 endif;
 endif;
 ?>
 ?>
 	</body>
 	</body>
-</html>
+</html>

+ 2 - 1
tpl/page.php

@@ -50,7 +50,7 @@ endif;
 ?>
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.1.js" integrity="sha512-ddI36MdUoXp/o7yhQtr9/qj4G3oFwCRga4jCGaoUYtORg0PPmFKVKG4Ess3fIknYzxwwKMlrIL9o4NwuPTCc1Q==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.1.js" integrity="sha512-ddI36MdUoXp/o7yhQtr9/qj4G3oFwCRga4jCGaoUYtORg0PPmFKVKG4Ess3fIknYzxwwKMlrIL9o4NwuPTCc1Q==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-rgDjBvJguZa4Liw2R/0vtFNZBSBTLhjdHROUseVyl+x1aVUbo5U8x/5lewWzYxrS9dxHOtgQkEWjx5ZM31JiNg==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-60d7BURzTV858Sry9vJEMgDXEqLT/IqQnjnx/iR3QfQ0LLWZI3bbM9nb4vbS0vQPYFVOP8lXpHRnNE7QEohsDw==" crossorigin="anonymous"></script>
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />
@@ -105,6 +105,7 @@ endif;
 					<button id="sendbutton" class="hidden"><img src="img/icon_send.png" width="18" height="15" alt="" /><?php echo I18n::_('Send'); ?></button>
 					<button id="sendbutton" class="hidden"><img src="img/icon_send.png" width="18" height="15" alt="" /><?php echo I18n::_('Send'); ?></button>
 					<button id="clonebutton" class="hidden"><img src="img/icon_clone.png" width="15" height="17" alt="" /><?php echo I18n::_('Clone'); ?></button>
 					<button id="clonebutton" class="hidden"><img src="img/icon_clone.png" width="15" height="17" alt="" /><?php echo I18n::_('Clone'); ?></button>
 					<button id="rawtextbutton" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" /><?php echo I18n::_('Raw text'); ?></button>
 					<button id="rawtextbutton" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" /><?php echo I18n::_('Raw text'); ?></button>
+					<button id="emaillink" class="hidden"><img src="img/icon_email.png" width="15" height="15" alt="" /><?php echo I18n::_('Email'); ?></button>
 <?php
 <?php
 if ($QRCODE):
 if ($QRCODE):
 ?>
 ?>