From 168ad8bf1b46fc1edc5fb1388e0ce86e2b73c9ef Mon Sep 17 00:00:00 2001
From: binwiederhier <philipp.heckel@gmail.com>
Date: Sun, 21 May 2023 20:56:56 -0400
Subject: [PATCH] Support encoding any header as RFC 2047

---
 docs/publish.md       | 18 +++++++++++++-----
 docs/releases.md      | 11 +++++++++--
 server/server.go      | 10 +++++-----
 server/server_test.go | 11 +++++++++--
 server/types.go       |  1 +
 server/util.go        |  2 +-
 6 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/docs/publish.md b/docs/publish.md
index 80d05d17..41801241 100644
--- a/docs/publish.md
+++ b/docs/publish.md
@@ -393,8 +393,8 @@ you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
 
 !!! info
     ntfy supports UTF-8 in HTTP headers, but [not every library or programming language does](https://www.jmix.io/blog/utf-8-in-http-headers/).
-    If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode the `X-Title` or `X-Message`
-    header as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
+    If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode any header (including the title)
+    as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
     or `=?UTF-8?Q?=C3=84pfel?=` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
 
 ## Message priority
@@ -619,7 +619,7 @@ them with a comma, e.g. `tag1,tag2,tag3`.
 
 !!! info
     ntfy supports UTF-8 in HTTP headers, but [not every library or programming language does](https://www.jmix.io/blog/utf-8-in-http-headers/).
-    If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode the individual tags
+    If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode the tags header or individual tags
     as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `tag1,=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
     or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
 
@@ -1004,9 +1004,11 @@ all the supported fields:
 | `actions`  | -        | *JSON array*                     | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications       |
 | `click`    | -        | *URL*                            | `https://example.com`                     | Website opened when notification is [clicked](#click-action)          |
 | `attach`   | -        | *URL*                            | `https://example.com/file.jpg`            | URL of an attachment, see [attach via URL](#attach-file-from-url)     |
+| `icon`     | -        | *string*                         | `https://example.com/icon.png`            | URL to use as notification [icon](#icons)                             |
 | `filename` | -        | *string*                         | `file.jpg`                                | File name of the attachment                                           |
 | `delay`    | -        | *string*                         | `30min`, `9am`                            | Timestamp or duration for delayed delivery                            |
 | `email`    | -        | *e-mail address*                 | `phil@example.com`                        | E-mail address for e-mail notifications                               |
+| `call`     | -        | *phone number or 'yes'*          | `+1222334444` or `yes`                    | Phone number to use for [voice call](#phone-calls)                    |
 
 ## Action buttons
 _Supported on:_ :material-android: :material-apple: :material-firefox:
@@ -1139,7 +1141,13 @@ As an example, here's how you can create the above notification using this forma
         ]
     ]));
     ```
- 
+
+!!! info
+    ntfy supports UTF-8 in HTTP headers, but [not every library or programming language does](https://www.jmix.io/blog/utf-8-in-http-headers/).
+    If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode any header (including actions) 
+    as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
+    or `=?UTF-8?Q?=C3=84pfel?=` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
+
 #### Using a JSON array
 Alternatively, the same actions can be defined as **JSON array**, if the notification is defined as part of the JSON body 
 (see [publish as JSON](#publish-as-json)):
@@ -3465,7 +3473,7 @@ table in their canonical form.
 
 !!! info
     ntfy supports UTF-8 in HTTP headers, but [not every library or programming language does](https://www.jmix.io/blog/utf-8-in-http-headers/).
-    If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode the `X-Title` or `X-Message`
+    If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode any
     header as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
     or `=?UTF-8?Q?=C3=84pfel?=` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
 
diff --git a/docs/releases.md b/docs/releases.md
index 633fa15f..89ecc516 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -28,7 +28,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source.
 * Attachments with filenames that are downloaded using a browser will now download with the proper filename ([#726](https://github.com/binwiederhier/ntfy/issues/726), thanks to [@un99known99](https://github.com/un99known99) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
 * Fix web app i18n issue in account preferences ([#730](https://github.com/binwiederhier/ntfy/issues/730), thanks to [@codebude](https://github.com/codebude) for reporting)
 
-### ntfy server v2.4.0
+## ntfy server v2.4.0
 Released Apr 26, 2023
 
 This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds suport to encode the `X-Title`,
@@ -57,7 +57,7 @@ will always remain open source.
 
 * Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/Shjosan/))
 
-### ntfy server v2.3.1 
+## ntfy server v2.3.1 
 Released March 30, 2023
 
 This release disables server-initiated polling of iOS devices entirely, thereby eliminating the thundering herd problem
@@ -1219,3 +1219,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 **Additional languages:**
 
 * Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
+
+### ntfy server v2.6.0 (UNRELEASED)
+
+**Bug fixes + maintenance:**
+
+* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
+
diff --git a/server/server.go b/server/server.go
index a451baa7..14a8c7f1 100644
--- a/server/server.go
+++ b/server/server.go
@@ -876,7 +876,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
 func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) {
 	cache = readBoolParam(r, true, "x-cache", "cache")
 	firebase = readBoolParam(r, true, "x-firebase", "firebase")
-	m.Title = maybeDecodeHeader(readParam(r, "x-title", "title", "t"))
+	m.Title = readParam(r, "x-title", "title", "t")
 	m.Click = readParam(r, "x-click", "click")
 	icon := readParam(r, "x-icon", "icon")
 	filename := readParam(r, "x-filename", "filename", "file", "f")
@@ -923,7 +923,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
 	}
 	messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
 	if messageStr != "" {
-		m.Message = maybeDecodeHeader(messageStr)
+		m.Message = messageStr
 	}
 	var e error
 	m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
@@ -931,9 +931,6 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
 		return false, false, "", "", false, errHTTPBadRequestPriorityInvalid
 	}
 	m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
-	for i, t := range m.Tags {
-		m.Tags[i] = maybeDecodeHeader(t)
-	}
 	delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
 	if delayStr != "" {
 		if !cache {
@@ -1747,6 +1744,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
 		if m.Delay != "" {
 			r.Header.Set("X-Delay", m.Delay)
 		}
+		if m.Call != "" {
+			r.Header.Set("X-Call", m.Call)
+		}
 		return next(w, r, v)
 	}
 }
diff --git a/server/server_test.go b/server/server_test.go
index 35d89707..fe84b856 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -2478,18 +2478,25 @@ func TestServer_PublishWithUTF8MimeHeader(t *testing.T) {
 	s := newTestServer(t, newTestConfig(t))
 
 	response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{
-		"X-Filename": "some attachment.txt",
+		"X-Filename": "some =?UTF-8?q?=C3=A4?=ttachment.txt",
 		"X-Message":  "=?UTF-8?B?8J+HqfCfh6o=?=",
 		"X-Title":    "=?UTF-8?B?bnRmeSDlvojmo5I=?=, no really I mean it! =?UTF-8?Q?This is q=C3=BC=C3=B6ted-print=C3=A4ble.?=",
 		"X-Tags":     "=?UTF-8?B?8J+HqfCfh6o=?=, =?UTF-8?B?bnRmeSDlvojmo5I=?=",
+		"X-Click":    "=?uTf-8?b?aHR0cHM6Ly/wn5KpLmxh?=",
+		"X-Actions":  "http, \"=?utf-8?q?Mettre =C3=A0 jour?=\", \"https://my.tld/webhook/netbird-update\"; =?utf-8?b?aHR0cCwg6L+Z5piv5LiA5Liq5qCH562+LCBodHRwczovL/CfkqkubGE=?=",
 	})
 	require.Equal(t, 200, response.Code)
 	m := toMessage(t, response.Body.String())
 	require.Equal(t, "🇩🇪", m.Message)
 	require.Equal(t, "ntfy 很棒, no really I mean it! This is qüöted-printäble.", m.Title)
-	require.Equal(t, "some attachment.txt", m.Attachment.Name)
+	require.Equal(t, "some ättachment.txt", m.Attachment.Name)
 	require.Equal(t, "🇩🇪", m.Tags[0])
 	require.Equal(t, "ntfy 很棒", m.Tags[1])
+	require.Equal(t, "https://💩.la", m.Click)
+	require.Equal(t, "Mettre à jour", m.Actions[0].Label)
+	require.Equal(t, "http", m.Actions[1].Action)
+	require.Equal(t, "这是一个标签", m.Actions[1].Label)
+	require.Equal(t, "https://💩.la", m.Actions[1].URL)
 }
 
 func TestServer_UpstreamBaseURL_Success(t *testing.T) {
diff --git a/server/types.go b/server/types.go
index 4280f6c9..9e4ff558 100644
--- a/server/types.go
+++ b/server/types.go
@@ -101,6 +101,7 @@ type publishMessage struct {
 	Attach   string   `json:"attach"`
 	Filename string   `json:"filename"`
 	Email    string   `json:"email"`
+	Call     string   `json:"call"`
 	Delay    string   `json:"delay"`
 }
 
diff --git a/server/util.go b/server/util.go
index a3a45547..03eb8661 100644
--- a/server/util.go
+++ b/server/util.go
@@ -50,7 +50,7 @@ func readParam(r *http.Request, names ...string) string {
 
 func readHeaderParam(r *http.Request, names ...string) string {
 	for _, name := range names {
-		value := r.Header.Get(name)
+		value := maybeDecodeHeader(r.Header.Get(name))
 		if value != "" {
 			return strings.TrimSpace(value)
 		}