# <enter-command> source '$my_confdir/buildmimetree.py \
# --tempdir $tempdir --extensions $my_mdwn_extensions \
# --css-file $my_confdir/htmlmail.css |'<enter>\
-# <enter-command> sourc e \$my_mdwn_postprocess_cmd_file<enter>\
+# <enter-command> source \$my_mdwn_postprocess_cmd_file<enter>\
# " "Convert message into a modern MIME tree with inline images"
#
# (Yes, we need to call source twice, as mutt only starts to process output
# Latest version:
# https://git.madduck.net/etc/neomutt.git/blob_plain/HEAD:/.config/neomutt/buildmimetree.py
#
-# Copyright © 2023 martin f. krafft <madduck@madduck.net>
-# Released under the GPL-2+ licence, just like Mutt itself.
+# Copyright © 2023–24 martin f. krafft <madduck@madduck.net>
+# Released under the GPL-2+ licence, just like NeoMutt itself.
#
import sys
import xml.etree.ElementTree as etree
import io
import enum
+import warnings
from contextlib import contextmanager
from collections import namedtuple, OrderedDict
from markdown.extensions import Extension
from markdown.blockprocessors import BlockProcessor
-from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE
+from markdown.inlinepatterns import (
+ SimpleTextInlineProcessor,
+ ImageInlineProcessor,
+ IMAGE_LINK_RE,
+)
from email.utils import make_msgid
from urllib import request
)
)
parser.epilog = (
- "Copyright © 2023 martin f. krafft <madduck@madduck.net>.\n"
+ "Copyright © 2023-24 martin f. krafft <madduck@madduck.net>.\n"
"Released under the MIT licence"
)
help="Only build, don't send the message",
)
+ parser.add_argument(
+ "--domain",
+ help="Domain to use in content IDs",
+ )
+
parser.add_argument(
"--tempdir",
metavar="DIR",
class File:
-
class Op(enum.Enum):
R = enum.auto()
W = enum.auto()
if content and not re.search(r"[r+]", mode):
raise RuntimeError("Cannot specify content without read mode")
- self._cache = {
- File.Op.R: [content] if content else [],
- File.Op.W: []
- }
+ self._cache = {File.Op.R: [content] if content else [], File.Op.W: []}
self._lastop = None
self._mode = mode
self._kwargs = kwargs
return self._file.read()
def write(self, s, *, cache=True):
-
if self._lastop == File.Op.R:
try:
self._file.seek(0)
def __init__(self):
self._images = OrderedDict()
- def register(self, path, description=None):
+ def register(self, path, description=None, *, domain=None):
# path = str(pathlib.Path(path).expanduser())
path = os.path.expanduser(path)
if path.startswith("/"):
path = f"file://{path}"
- cid = make_msgid()[1:-1]
+ cid = make_msgid(domain=domain)[1:-1]
self._images[path] = InlineImageInfo(cid, description)
return cid
try:
- import pynliner
+ with warnings.catch_warnings():
+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1081037
+ warnings.filterwarnings("ignore", category=SyntaxWarning)
+ import pynliner
_PYNLINER = True
)
+# [ FORMAT=FLOWED HANDLING ] ##################################################
+
+
+class FormatFlowedNewlineExtension(Extension):
+ FFNL_RE = r"(?!\S)(\s)\n"
+
+ def extendMarkdown(self, md):
+ ffnl = SimpleTextInlineProcessor(self.FFNL_RE)
+ md.inlinePatterns.register(ffnl, "ffnl", 125)
+
+
# [ QUOTE HANDLING ] ##########################################################
class QuoteToAdmonitionExtension(Extension):
- class EmailQuoteBlockProcessor(BlockProcessor):
+ class BlockProcessor(BlockProcessor):
RE = re.compile(r"(?:^|\n)>\s*(.*)")
def __init__(self, parser):
admonition[0].set("class", "admonition-title")
with self.disable():
- self.parser.parseChunk(
- admonition, "\n".join(quotelines)
- )
+ self.parser.parseChunk(admonition, "\n".join(quotelines))
@contextmanager
def disable(self):
def extendMarkdown(self, md):
md.registerExtension(self)
- email_quote_proc = self.EmailQuoteBlockProcessor(md.parser)
+ email_quote_proc = self.BlockProcessor(md.parser)
md.parser.blockprocessors.register(email_quote_proc, "emailquote", 25)
tempdir=None,
extensions=None,
extension_configs=None,
+ domain=None,
):
# TODO extension_configs need to be handled differently
extension_configs = extension_configs or {}
] = _CODEHILITE_CLASS
extensions = extensions or []
+ extensions.append(FormatFlowedNewlineExtension())
extensions.append(QuoteToAdmonitionExtension())
draft = draft_f.read()
for img in soup.find_all("img"):
uri = img.attrs["src"]
desc = img.attrs.get("title", img.attrs.get("alt"))
- cid = image_registry.register(uri, desc)
+ cid = image_registry.register(uri, desc, domain=domain)
img.attrs["src"] = f"cid:{cid}"
htmlsig = str(soup)
only_build=False,
max_other_attachments=20,
tempdir=None,
+ domain=None,
debug_commands=False,
debug_walk=False,
):
related_to_html_only=related_to_html_only,
tempdir=tempdir,
extensions=extensions,
+ domain=domain,
)
mimetree = MIMETreeDFWalker(debug=debug_walk)
max_other_attachments=args.max_number_other_attachments,
only_build=args.only_build,
tempdir=args.tempdir,
+ domain=args.domain,
debug_commands=args.debug_commands,
debug_walk=args.debug_walk,
)
with (
File() as draft_f,
File(mode="w") as cmd_f,
- File(content=css) as css_f
+ File(content=css) as css_f,
):
do_massage(
draft_f=draft_f,
assert not cid.endswith(">")
assert const1 in reg
+ @pytest.mark.imgproc
+ def test_image_registry_domain(self, const1, const2):
+ reg = ImageRegistry()
+ cid = reg.register(const1, domain=const2)
+ assert f"@{const2}" in cid
+ assert not cid.startswith("<")
+ assert not cid.endswith(">")
+ assert const1 in reg
+
@pytest.mark.imgproc
def test_image_registry_file_uri(self, const1):
reg = ImageRegistry()
"This is the plain-text version",
)
htmlsig = "HTML Signature from {path} but as a string"
- html = (
- f'<div id="signature"><p>{htmlsig.format(path=fakepath2)}</p></div>'
- )
+ html = f'<div id="signature"><p>{htmlsig.format(path=fakepath2)}</p></div>'
sig_f = fakefilefactory(fakepath2, content=html)
== mailparts[-2]
)
+ @pytest.mark.converter
+ def test_converter_format_flowed_with_nl2br(
+ self, fakepath, fakefilefactory
+ ):
+ mailparts = (
+ "This is format=flowed text ",
+ "with spaces at the end ",
+ "and there ought be no newlines.",
+ "",
+ "[link](https://example.org) ",
+ "and text.",
+ "",
+ "[link text ",
+ "broken up](https://example.org).",
+ "",
+ "This is on a new line with a hard break ",
+ "due to the double space",
+ )
+ with fakefilefactory(
+ fakepath, content="\n".join(mailparts)
+ ) as draft_f:
+ convert_markdown_to_html(
+ draft_f, extensions=["nl2br"], filefactory=fakefilefactory
+ )
+
+ soup = bs4.BeautifulSoup(
+ fakefilefactory[fakepath.with_suffix(".html")].read(),
+ "html.parser",
+ )
+ import ipdb
+
+ p = soup.p.extract().text
+ assert "".join(mailparts[0:3]) == p
+ p = ''.join(map(str, soup.p.extract().contents))
+ assert p == '<a href="https://example.org">link</a> and text.'
+ p = ''.join(map(str, soup.p.extract().contents))
+ assert (
+ p == '<a href="https://example.org">link text broken up</a>.'
+ )
+
@pytest.mark.fileio
def test_file_class_contextmanager(self, const1, monkeypatch):
state = dict(o=False, c=False)