diff --git a/src/content/scripts/editemailsubject.mjs b/src/content/scripts/editemailsubject.mjs index c42513b..f276d45 100644 --- a/src/content/scripts/editemailsubject.mjs +++ b/src/content/scripts/editemailsubject.mjs @@ -16,6 +16,8 @@ * Modifications for TB78, TB91, TB102, TB115 by John Bieling (2020-2023) */ +import * as q from "/content/scripts/q.mjs"; + // Reduces a MessageList to a single message, if it contains a single message. export function getSingleMessageFromList(messageList) { return messageList && messageList.messages && messageList.messages.length == 1 @@ -71,11 +73,34 @@ export async function updateMessage({ msgId, tabId, keepBackup, newSubject, curr while (headerPart.match(/\r\nSubject: .*\r\n\s+/)) headerPart = headerPart.replace(/(\r\nSubject: .*)(\r\n\s+)/, "$1 "); + // RFC2047 Q-Encoding + const encodeHeader = (name, value) => { + const startLine = " =?utf-8?q?" + const nextLine = "=?=" + const endLine = "?="; + + let lines = []; + let currentLine = `${name}:${startLine}`; + + let uint8Array = new TextEncoder().encode(value); + for (let v of uint8Array.values()) { + let next = q.encode(String.fromCharCode(v)); // or btoa() for b-encoding + if (currentLine.length + next.length + endLine.length > 78) { + lines.push(currentLine + nextLine); + currentLine = startLine; + } + currentLine += next; + } + lines.push(currentLine + endLine); + + return lines.join("\r\n"); + } + // Either replace the subject header or add one if missing. if (headerPart.includes("\nSubject: ")) { - headerPart = headerPart.replace(/\nSubject: .*\r\n/, "\nSubject: " + unescape(encodeURIComponent(newSubject)) + "\r\n"); + headerPart = headerPart.replace(/\nSubject: .*\r\n/, "\n" + encodeHeader("Subject", newSubject) + "\r\n"); } else { - headerPart += "Subject: " + unescape(encodeURIComponent(newSubject)) + "\r\n"; + headerPart += encodeHeader("Subject", newSubject) + "\r\n"; } // Without changing the message-id, the MessageHeader obj of the new message and the old message will diff --git a/src/content/scripts/q.mjs b/src/content/scripts/q.mjs new file mode 100644 index 0000000..2a7ceac --- /dev/null +++ b/src/content/scripts/q.mjs @@ -0,0 +1,41 @@ +/*! https://mths.be/q v1.0.0 by @mathias | MIT license */ + +// https://tools.ietf.org/html/rfc2047#section-4.2 +var stringFromCharCode = String.fromCharCode; +export function decode (input) { + return input + // Decode `_` into a space. This is character-encoding-independent; + // see https://tools.ietf.org/html/rfc2047#section-4.2, item 2. + .replace(/_/g, ' ') + // Decode escape sequences of the form `=XX` where `XX` is any + // combination of two hexidecimal digits. For optimal compatibility, + // lowercase hexadecimal digits are supported as well. See + // https://tools.ietf.org/html/rfc2045#section-6.7, note 1. + .replace(/=([a-fA-F0-9]{2})/g, function ($0, $1) { + var codePoint = parseInt($1, 16); + return stringFromCharCode(codePoint); + }); +}; + +var regexUnsafeSymbols = /[\0-\x1F"-\),\.:-@\[-\^`\{-\uFFFF]/g; +export function encode (string) { + // Note: this assumes the input is already encoded into octets (e.g. using + // UTF-8), and that the resulting octets are within the extended ASCII + // range. + return string + // Encode symbols that are definitely unsafe (i.e. unsafe in any context). + .replace(regexUnsafeSymbols, function (symbol) { + if (symbol > '\xFF') { + throw RangeError( + '`q.encode()` expects extended ASCII input only. Don\u2019t ' + + 'forget to encode the input first using a character encoding ' + + 'like UTF-8.' + ); + } + var codePoint = symbol.charCodeAt(0); + var hexadecimal = codePoint.toString(16).toUpperCase(); + return '=' + ('0' + hexadecimal).slice(-2); + }) + // Encode spaces as `_`, as it’s shorter than `=20`. + .replace(/\x20/g, '_'); +};