mirror of
https://github.com/tutao/tutanota.git
synced 2025-12-08 06:09:50 +00:00
add support for Q-encoded list unsubscribe headers
We now successfully parse MIME Q-encoded list unsubscribe headers and can unsubscribe from such newsletters.
This commit is contained in:
parent
5293be6a4a
commit
c2c8f934c5
2 changed files with 34 additions and 12 deletions
|
|
@ -675,6 +675,20 @@ export class MailViewerViewModel {
|
|||
return !isEmpty(listUnsubscribeHeaders)
|
||||
}
|
||||
|
||||
private decodeMimeHeader(value: string): string {
|
||||
return value.replace(/=\?([^?]+)\?([QB])\?([^?]+)\?=/gi, (_, _charset, encoding, encodedText) => {
|
||||
if (encoding.toUpperCase() === "Q") {
|
||||
return encodedText.replace(/_/g, " ").replace(/=([A-Fa-f0-9]{2})/g, (_: string, hex: string) => String.fromCharCode(parseInt(hex, 16)))
|
||||
} else if (encoding.toUpperCase() === "B") {
|
||||
try {
|
||||
return Buffer.from(encodedText, "base64").toString("utf-8")
|
||||
} catch {
|
||||
return encodedText
|
||||
}
|
||||
}
|
||||
return encodedText
|
||||
})
|
||||
}
|
||||
async determineUnsubscribeOrder(): Promise<Array<UnsubscribeAction>> {
|
||||
const mailHeaders = await this.getHeaders()
|
||||
const unsubscribeActions: Array<UnsubscribeAction> = []
|
||||
|
|
@ -682,23 +696,19 @@ export class MailViewerViewModel {
|
|||
return unsubscribeActions
|
||||
}
|
||||
|
||||
const listUnsubscribeHeaders = mailHeaders
|
||||
.replaceAll(/\r\n/g, "\n") // replace all CR LF with LF
|
||||
.replaceAll(/\n[ \t]/g, "") // join multiline headers to a single line
|
||||
.split("\n") // split headers
|
||||
.filter((headerLine) => headerLine.toLowerCase().startsWith("list-unsubscribe:"))
|
||||
const normalizedHeaders = mailHeaders
|
||||
.replaceAll(/\r\n/g, "\n")
|
||||
.replaceAll(/\n[ \t]/g, "")
|
||||
.split("\n")
|
||||
.map((h) => this.decodeMimeHeader(h.trim()))
|
||||
|
||||
const listUnsubscribeHeaders = normalizedHeaders.filter((headerLine) => headerLine.toLowerCase().startsWith("list-unsubscribe:"))
|
||||
|
||||
if (isEmpty(listUnsubscribeHeaders)) {
|
||||
return unsubscribeActions
|
||||
}
|
||||
|
||||
const unsubPostHeader = first(
|
||||
mailHeaders
|
||||
.replaceAll(/\r\n/g, "\n") // replace all CR LF with LF
|
||||
.replaceAll(/\n[ \t]/g, "") // join multiline headers to a single line
|
||||
.split("\n") // split headers
|
||||
.filter((headerLine) => headerLine.toLowerCase().startsWith("list-unsubscribe-post")),
|
||||
)
|
||||
const unsubPostHeader = normalizedHeaders.find((h) => h.toLowerCase().startsWith("list-unsubscribe-post"))
|
||||
|
||||
const [_, ...value] = listUnsubscribeHeaders[0].split(":")
|
||||
const headerValue = value.join(":")
|
||||
|
|
|
|||
|
|
@ -306,6 +306,18 @@ o.spec("MailViewerViewModel", function () {
|
|||
const expectedPostUrl = "http://unsub.me?id=2134"
|
||||
await testHeaderUnsubscribePost(headers.join("\r\n"), expectedPostUrl, true)
|
||||
})
|
||||
o("with Q encoded header", async function () {
|
||||
const headers = [
|
||||
"List-Unsubscribe: ",
|
||||
" =?us-ascii?Q?=3Chttps=3A=2F=2Fnewsletter=2Eexample=2Ecom=2Funsubscribe=2Fabcd1234efgh?=",
|
||||
" =?us-ascii?Q?5678ijkl91011mnopqr=3E?=",
|
||||
"List-Unsubscribe-Post: List-Unsubscribe=One-Click",
|
||||
]
|
||||
|
||||
const expectedPostUrl = "https://newsletter.example.com/unsubscribe/abcd1234efgh5678ijkl91011mnopqr"
|
||||
|
||||
await testHeaderUnsubscribePost(headers.join("\r\n"), expectedPostUrl, true)
|
||||
})
|
||||
o("no list unsubscribe header", async function () {
|
||||
const headers = "To: InvalidHeader"
|
||||
const viewModel = initUnsubscribeHeaders(headers)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue