Skip to content

Commit

Permalink
[ntfy] Improve RFC 2047 header value encoding
Browse files Browse the repository at this point in the history
Only apply the encoding to the values of ntfy option fields `title`,
`message`, and `tags`. Strip non-ASCII characters from all others.
  • Loading branch information
amotl committed Apr 26, 2023
1 parent 4b3b054 commit 08b4355
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 7 deletions.
30 changes: 24 additions & 6 deletions mqttwarn/services/ntfy.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
"unifiedpush",
]

NTFY_RFC2047_FIELDS: t.List[str] = [
"message",
"title",
"tags",
]

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -77,7 +83,7 @@ def to_http_headers(self) -> t.Dict[str, str]:
In this spirit, the header transport does not permit any fancy UTF-8 characters
within any field, so they will be replaced with placeholder characters `?`.
"""
return dict_rfc2047(dict_with_titles(self.fields))
return dict_with_titles(encode_ntfy_fields(self.fields))


def plugin(srv: Service, item: ProcessorItem) -> bool:
Expand Down Expand Up @@ -231,10 +237,19 @@ def dict_ascii_clean(data: DataDict) -> t.Dict[str, str]:
return outdata


def dict_rfc2047(data: DataDict) -> t.Dict[str, str]:
def encode_ntfy_fields(data: DataDict) -> t.Dict[str, str]:
"""
Return dictionary using values encoded with RFC 2047, aka. MIME Message
Header Extensions for Non-ASCII Text. Two encodings are possible.
Return dictionary suitable for submitting to the ntfy HTTP API using HTTP headers.
- The field values for `title`, `message` and `tags` are encoded using RFC 2047, aka.
MIME Message Header Extensions for Non-ASCII Text.
- The other field values will be stripped from any special characters to be ASCII-clean.
Appendix
When using RFC 2047, two encodings are possible. The Python implementation cited below
seems to use the "Q" encoding scheme by default.
4.1 The "B" encoding is identical to the "BASE64" encoding defined by RFC 2045.
4.2 The "Q" encoding is similar to the "Quoted-Printable" content-transfer-encoding
Expand All @@ -252,12 +267,15 @@ def dict_rfc2047(data: DataDict) -> t.Dict[str, str]:
outdata = OrderedDict()
for key, value in data.items():
key = ascii_clean(key).strip()
value = encode_rfc2047(value)
if key in NTFY_RFC2047_FIELDS:
value = encode_rfc2047(value)
else:
value = ascii_clean(value)
outdata[key] = value
return outdata


def dict_with_titles(data: DataDict) -> DataDict:
def dict_with_titles(data: t.Dict[str, str]) -> t.Dict[str, str]:
"""
Return dictionary with each key title-cased, i.e. uppercasing the first letter.
Expand Down
11 changes: 10 additions & 1 deletion tests/services/test_ntfy.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,12 @@ def test_ntfy_plugin_success(srv, caplog, attachment_dummy):
addrs={"url": "http://localhost:9999/testdrive", "file": attachment_dummy.name},
title="⚽ Message title ⚽",
message="⚽ Notification message ⚽",
data={"priority": "high", "tags": "foo,bar,äöü", "click": "https://example.org/testdrive"},
data={
"priority": "high",
"tags": "foo,bar,äöü",
"click": "https://example.org/testdrive",
"actions": "view, Adjust temperature 🌡, https://example.org/home-automation/temperature, body='{\"temperature\": 18}'", # noqa: E501
},
)

outcome = module.plugin(srv, item)
Expand All @@ -304,6 +309,10 @@ def test_ntfy_plugin_success(srv, caplog, attachment_dummy):
assert response.request.body.read() == b"foo"
assert response.request.headers["User-Agent"] == "mqttwarn"
assert response.request.headers["Tags"] == "=?utf-8?q?foo=2Cbar=2C=C3=A4=C3=B6=C3=BC?="
assert (
response.request.headers["Actions"]
== "view, Adjust temperature ?, https://example.org/home-automation/temperature, body='{\"temperature\": 18}'"
)

assert response.response.status_code == 200
assert response.response.json() == ntfy_api_response
Expand Down

0 comments on commit 08b4355

Please sign in to comment.