#     set my_mdwn_extensions="extra,admonition,codehilite,sane_lists,smarty"
 #     macro compose B "\
 #       <enter-command> source '$my_confdir/buildmimetree.py \
-#       --tempdir $tempdir --extensions $my_mdwn_extensions|'<enter>\
+#       --tempdir $tempdir --extensions $my_mdwn_extensions \
+#       --css-file $my_confdir/htmlmail.css |'<enter>\
 #       <enter-command> sourc e \$my_mdwn_postprocess_cmd_file<enter>\
 #     " "Convert message into a modern MIME tree with inline images"
 #
 # Requirements:
 #   - python3
 #   - python3-markdown
+#   - python3-beautifulsoup4
 # Optional:
 #   - pytest
 #   - Pynliner, provides --css-file and thus inline styling of HTML output
 #
 
 import sys
+import os
+import os.path
 import pathlib
 import markdown
 import tempfile
 import argparse
 import re
 import mimetypes
+import bs4
 from collections import namedtuple, OrderedDict
 from markdown.extensions import Extension
 from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE
 
     parser.add_argument(
         "--extensions",
+        metavar="EXT[,EXT[,EXT]]",
         type=str,
         default="",
         help="Markdown extension to use (comma-separated list)",
     if _PYNLINER:
         parser.add_argument(
             "--css-file",
+            metavar="FILE",
             type=pathlib.Path,
+            default=os.devnull,
             help="CSS file to merge with the final HTML",
         )
     else:
         except ValueError:
             pass
 
-        raise ValueError(f"Must be a positive integer")
+        raise ValueError("Must be a positive integer")
 
     parser.add_argument(
         "--max-number-other-attachments",
+        metavar="INTEGER",
         type=positive_integer,
-        help="Make related content be sibling to HTML parts only",
+        default=20,
+        help="Maximum number of other attachments to expect",
     )
 
     parser.add_argument(
         "--only-build",
+        "--just-build",
         action="store_true",
         help="Only build, don't send the message",
     )
 
     parser.add_argument(
         "--tempdir",
+        metavar="DIR",
         type=pathlib.Path,
         help="Specify temporary directory to use for attachments",
     )
         help="Turn on debug logging of commands generated to stderr",
     )
 
+    parser.add_argument(
+        "--debug-walk",
+        action="store_true",
+        help="Turn on debugging to stderr of the MIME tree walk",
+    )
+
     subp = parser.add_subparsers(help="Sub-command parsers", dest="mode")
     massage_p = subp.add_parser(
         "massage", help="Massaging phase (internal use)"
     massage_p.add_argument(
         "--write-commands-to",
         "-o",
-        metavar="PATH",
+        metavar="FILE",
         dest="cmdpath",
         type=pathlib.Path,
         required=True,
         help="Temporary file path to write commands to",
     )
 
-    massage_p.add_argument(
-        "--debug-walk",
-        action="store_true",
-        help="Turn on debugging to stderr of the MIME tree walk",
-    )
-
     massage_p.add_argument(
         "MAILDRAFT",
         nargs="?",
     return parser.parse_args(*args, **kwargs)
 
 
-# [ MARKDOWN WRAPPING ] #######################################################
+# [ IMAGE HANDLING ] ##########################################################
 
 
 InlineImageInfo = namedtuple(
 )
 
 
+class ImageRegistry:
+    def __init__(self):
+        self._images = OrderedDict()
+
+    def register(self, path, description=None):
+        path = os.path.expanduser(path)
+        if path.startswith("/"):
+            path = f"file://{path}"
+        cid = make_msgid()[1:-1]
+        self._images[path] = InlineImageInfo(cid, description)
+        return cid
+
+    def __iter__(self):
+        return self._images.__iter__()
+
+    def __getitem__(self, idx):
+        return self._images.__getitem__(idx)
+
+    def __len__(self):
+        return self._images.__len__()
+
+    def items(self):
+        return self._images.items()
+
+    def __repr__(self):
+        return f"<ImageRegistry(items={len(self._images)})>"
+
+    def __str__(self):
+        return self._images.__str__()
+
+
 class InlineImageExtension(Extension):
     class RelatedImageInlineProcessor(ImageInlineProcessor):
-        def __init__(self, re, md, ext):
+        def __init__(self, re, md, registry):
             super().__init__(re, md)
-            self._ext = ext
+            self._registry = registry
 
         def handleMatch(self, m, data):
             el, start, end = super().handleMatch(m, data)
                 src = el.attrib["src"]
                 if "://" not in src or src.startswith("file://"):
                     # We only inline local content
-                    cid = self._ext.get_cid_for_image(el.attrib)
+                    cid = self._registry.register(
+                        el.attrib["src"],
+                        el.attrib.get("title", el.attrib.get("alt")),
+                    )
                     el.attrib["src"] = f"cid:{cid}"
             return el, start, end
 
-    def __init__(self):
+    def __init__(self, registry):
         super().__init__()
-        self._images = OrderedDict()
+        self._image_registry = registry
+
+    INLINE_PATTERN_NAME = "image_link"
 
     def extendMarkdown(self, md):
         md.registerExtension(self)
         inline_image_proc = self.RelatedImageInlineProcessor(
-            IMAGE_LINK_RE, md, self
+            IMAGE_LINK_RE, md, self._image_registry
         )
-        md.inlinePatterns.register(inline_image_proc, "image_link", 150)
-
-    def get_cid_for_image(self, attrib):
-        msgid = make_msgid()[1:-1]
-        path = attrib["src"]
-        if path.startswith("/"):
-            path = f"file://{path}"
-        self._images[path] = InlineImageInfo(
-            msgid, attrib.get("title", attrib.get("alt"))
+        md.inlinePatterns.register(
+            inline_image_proc, InlineImageExtension.INLINE_PATTERN_NAME, 150
         )
-        return msgid
-
-    def get_images(self):
-        return self._images
 
 
 def markdown_with_inline_image_support(
-    text, *, extensions=None, extension_configs=None
+    text,
+    *,
+    mdwn=None,
+    image_registry=None,
+    extensions=None,
+    extension_configs=None,
 ):
-    inline_image_handler = InlineImageExtension()
+    registry = (
+        image_registry if image_registry is not None else ImageRegistry()
+    )
+    inline_image_handler = InlineImageExtension(registry=registry)
     extensions = extensions or []
     extensions.append(inline_image_handler)
     mdwn = markdown.Markdown(
         extensions=extensions, extension_configs=extension_configs
     )
-    htmltext = mdwn.convert(text)
 
-    images = inline_image_handler.get_images()
+    htmltext = mdwn.convert(text)
 
     def replace_image_with_cid(matchobj):
         for m in (matchobj.group(1), f"file://{matchobj.group(1)}"):
-            if m in images:
-                return f"(cid:{images[m].cid}"
+            if m in registry:
+                return f"(cid:{registry[m].cid}"
         return matchobj.group(0)
 
     text = re.sub(r"\(([^)\s]+)", replace_image_with_cid, text)
-    return text, htmltext, images
+    return text, htmltext, registry, mdwn
 
 
 # [ CSS STYLING ] #############################################################
 
+
 try:
     import pynliner
 
         return hash(str(self.subtype) + "".join(str(self.children)))
 
 
+def filereader_fn(path, mode="r", **kwargs):
+    with open(path, mode, **kwargs) as in_f:
+        return in_f.read()
+
+
 def filewriter_fn(path, content, mode="w", **kwargs):
     with open(path, mode, **kwargs) as out_f:
         out_f.write(content)
 
 
 def collect_inline_images(
-    images, *, tempdir=None, filewriter_fn=filewriter_fn
+    image_registry, *, tempdir=None, filewriter_fn=filewriter_fn
 ):
     relparts = []
-    for path, info in images.items():
+    for path, info in image_registry.items():
         if path.startswith("cid:"):
             continue
 
 
         filewriter_fn(path, data.read(), "w+b")
 
+        desc = (
+            f'Inline image: "{info.desc}"'
+            if info.desc
+            else f"Inline image {str(len(relparts)+1)}"
+        )
         relparts.append(
-            Part(
-                *mimetype.split("/"),
-                path,
-                cid=info.cid,
-                desc=f"Image: {info.desc}",
-            )
+            Part(*mimetype.split("/"), path, cid=info.cid, desc=desc)
         )
 
     return relparts
 
 
+EMAIL_SIG_SEP = "\n-- \n"
+HTML_SIG_MARKER = "=htmlsig "
+
+
+def make_html_doc(body, sig=None):
+    ret = (
+        "<!DOCTYPE html>\n"
+        "<html>\n"
+        "<head>\n"
+        '<meta http-equiv="content-type" content="text/html; charset=UTF-8">\n'  # noqa: E501
+        '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'  # noqa: E501
+        "</head>\n"
+        "<body>\n"
+        f"{body}\n"
+    )
+
+    if sig:
+        nl = "\n"
+        ret = (
+            f'{ret}<div id="signature"><span class="sig_separator">{EMAIL_SIG_SEP.strip(nl)}</span>\n'  # noqa: E501
+            f"{sig}\n"
+            "</div>"
+        )
+
+    return f"{ret}\n  </body>\n</html>"
+
+
+def make_text_mail(text, sig=None):
+    return EMAIL_SIG_SEP.join((text, sig)) if sig else text
+
+
+def extract_signature(text, *, filereader_fn=filereader_fn):
+    parts = text.split(EMAIL_SIG_SEP, 1)
+    if len(parts) == 1:
+        return text, None, None
+
+    lines = parts[1].splitlines()
+    if lines[0].startswith(HTML_SIG_MARKER):
+        path = pathlib.Path(re.split(r" +", lines.pop(0), maxsplit=1)[1])
+        textsig = "\n".join(lines)
+
+        sig_input = filereader_fn(path.expanduser())
+        soup = bs4.BeautifulSoup(sig_input, "html.parser")
+
+        style = str(soup.style.extract()) if soup.style else ""
+        for sig_selector in (
+            "#signature",
+            "#signatur",
+            "#emailsig",
+            ".signature",
+            ".signatur",
+            ".emailsig",
+            "body",
+            "div",
+        ):
+            sig = soup.select_one(sig_selector)
+            if sig:
+                break
+
+        if not sig:
+            return parts[0], textsig, style + sig_input
+
+        if sig.attrs.get("id") == "signature":
+            sig = "".join(str(c) for c in sig.children)
+
+        return parts[0], textsig, style + str(sig)
+
+    return parts[0], parts[1], None
+
+
 def convert_markdown_to_html(
     origtext,
     draftpath,
     *,
     related_to_html_only=False,
-    cssfile=None,
+    css=None,
     filewriter_fn=filewriter_fn,
+    filereader_fn=filereader_fn,
     tempdir=None,
     extensions=None,
     extension_configs=None,
     extension_configs.setdefault("pymdownx.highlight", {})
     extension_configs["pymdownx.highlight"]["css_class"] = _CODEHILITE_CLASS
 
-    origtext, htmltext, images = markdown_with_inline_image_support(
+    origtext, textsig, htmlsig = extract_signature(
+        origtext, filereader_fn=filereader_fn
+    )
+
+    (
+        origtext,
+        htmltext,
+        image_registry,
+        mdwn,
+    ) = markdown_with_inline_image_support(
         origtext, extensions=extensions, extension_configs=extension_configs
     )
 
+    if htmlsig:
+        if not textsig:
+            # TODO: decide what to do if there is no plain-text version
+            raise NotImplementedError("HTML signature but no text alternative")
+
+        soup = bs4.BeautifulSoup(htmlsig, "html.parser")
+        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)
+            img.attrs["src"] = f"cid:{cid}"
+
+        htmlsig = str(soup)
+
+    elif textsig:
+        (
+            textsig,
+            htmlsig,
+            image_registry,
+            mdwn,
+        ) = markdown_with_inline_image_support(
+            textsig,
+            extensions=extensions,
+            extension_configs=extension_configs,
+            image_registry=image_registry,
+            mdwn=mdwn,
+        )
+
+    origtext = make_text_mail(origtext, textsig)
+
     filewriter_fn(draftpath, origtext, encoding="utf-8")
     textpart = Part(
         "text", "plain", draftpath, "Plain-text version", orig=True
     )
 
-    htmltext = apply_styling(htmltext, cssfile)
+    htmltext = make_html_doc(htmltext, htmlsig)
+    htmltext = apply_styling(htmltext, css)
 
     htmlpath = draftpath.with_suffix(".html")
     filewriter_fn(
     htmlpart = Part("text", "html", htmlpath, "HTML version")
 
     imgparts = collect_inline_images(
-        images, tempdir=tempdir, filewriter_fn=filewriter_fn
+        image_registry, tempdir=tempdir, filewriter_fn=filewriter_fn
     )
 
     if related_to_html_only:
             self._cmd1.append(s)
 
     def push(self, s):
-        s = s.replace('"', '"')
+        s = s.replace('"', r"\"")
         s = f'push "{s}"'
         self.debugprint(s)
         self._push.insert(0, s)
     cmd_f,
     *,
     extensions=None,
-    cssfile=None,
+    css_f=None,
     converter=convert_markdown_to_html,
     related_to_html_only=True,
     only_build=False,
     tree = converter(
         draft_f.read(),
         draftpath,
-        cssfile=cssfile,
+        css=css_f.read() if css_f else None,
         related_to_html_only=related_to_html_only,
         tempdir=tempdir,
         extensions=extensions,
                     # number of possible attachments. The performance
                     # difference of using a high number is negligible.
                     # Bubble up the new part
-                    cmds.push(f"<move-up>")
+                    cmds.push("<move-up>")
 
                 # As we push the part to the right position in the list (i.e.
                 # the last of the subset of attachments this script added), we
                 # is decremented by the number of descendents so far
                 # encountered.
                 for i in range(1, state["pos"] - len(descendents)):
-                    cmds.push(f"<move-down>")
+                    cmds.push("<move-down>")
 
         elif isinstance(item, Multipart):
             # This node has children, but we already visited them (see
     elif args.mode == "massage":
         with open(args.MAILDRAFT, "r") as draft_f, open(
             args.cmdpath, "w"
-        ) as cmd_f:
+        ) as cmd_f, open(args.css_file, "r") as css_f:
             do_massage(
                 draft_f,
                 args.MAILDRAFT,
                 cmd_f,
                 extensions=args.extensions,
-                cssfile=args.css_file,
+                css_f=css_f,
                 related_to_html_only=args.related_to_html_only,
                 max_other_attachments=args.max_number_other_attachments,
                 only_build=args.only_build,
         # NOTE: tests using the capsys fixture must specify sys.stdout to the
         # functions they call, else old stdout is used and not captured
 
+        @pytest.mark.muttctrl
         def test_MuttCommands_cmd(self, const1, const2, capsys):
             "Assert order of commands"
             cmds = MuttCommands(out_f=sys.stdout)
             captured = capsys.readouterr()
             assert captured.out == "\n".join((const1, const2, ""))
 
+        @pytest.mark.muttctrl
         def test_MuttCommands_push(self, const1, const2, capsys):
             "Assert reverse order of pushes"
             cmds = MuttCommands(out_f=sys.stdout)
                 == ('"\npush "'.join(("", const2, const1, "")))[2:-6]
             )
 
+        @pytest.mark.muttctrl
+        def test_MuttCommands_push_escape(self, const1, const2, capsys):
+            cmds = MuttCommands(out_f=sys.stdout)
+            cmds.push(f'"{const1}"')
+            cmds.flush()
+            captured = capsys.readouterr()
+            assert f'"\\"{const1}\\""' in captured.out
+
+        @pytest.mark.muttctrl
         def test_MuttCommands_cmd_push_mixed(self, const1, const2, capsys):
             "Assert reverse order of pushes"
             cmds = MuttCommands(out_f=sys.stdout)
                 desc="Alternative",
             )
 
+        @pytest.mark.treewalk
         def test_MIMETreeDFWalker_depth_first_walk(
             self, mime_tree_related_to_alternative
         ):
             assert items[4][1] == 0
             assert items[4][2] == 4
 
+        @pytest.mark.treewalk
         def test_MIMETreeDFWalker_list_to_mixed(self, const1):
             mimetree = MIMETreeDFWalker()
             items = []
             mimetree.walk([p, p], visitor_fn=visitor_fn)
             assert items[-1].subtype == "mixed"
 
+        @pytest.mark.treewalk
         def test_MIMETreeDFWalker_visitor_in_constructor(
             self, mime_tree_related_to_alternative
         ):
         def string_io(self, const1, text=None):
             return StringIO(text or const1)
 
+        @pytest.mark.massage
         def test_do_massage_basic(self, const1, string_io, capsys):
             def converter(
                 drafttext,
                 draftpath,
-                cssfile,
+                css,
                 related_to_html_only,
                 extensions,
                 tempdir,
             assert "source 'rm -f " in lines.pop(0)
             assert "unset my_mdwn_postprocess_cmd_file" == lines.pop(0)
 
+        @pytest.mark.massage
         def test_do_massage_fulltree(
             self, string_io, const1, mime_tree_related_to_alternative, capsys
         ):
             def converter(
                 drafttext,
                 draftpath,
-                cssfile,
+                css,
                 related_to_html_only,
                 extensions,
                 tempdir,
         def markdown_non_converter(self, const1, const2):
             return lambda s, text: f"{const1}{text}{const2}"
 
+        @pytest.mark.converter
         def test_converter_tree_basic(self, const1, const2, fake_filewriter):
             path = pathlib.Path(const2)
             tree = convert_markdown_to_html(
                 )
 
             assert (path, const1) == fake_filewriter.pop(0)
-            assert (
-                path.with_suffix(".html"),
-                markdown_non_converter(None, const1),
-            ) == fake_filewriter.pop(0)
+            written = fake_filewriter.pop(0)
+            assert path.with_suffix(".html") == written[0]
+            assert const1 in written[1]
 
+        @pytest.mark.imgproc
         def test_markdown_inline_image_processor(self):
             imgpath1 = "file:/path/to/image.png"
             imgpath2 = "file:///path/to/image.png?url=params"
                        
                        """
-            text, html, images = markdown_with_inline_image_support(text)
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
 
             # local paths have been normalised to URLs:
             imgpath3 = f"file://{imgpath3}"
             assert images[imgpath1].cid != images[imgpath3].cid
             assert images[imgpath2].cid != images[imgpath3].cid
 
+        @pytest.mark.imgproc
         def test_markdown_inline_image_processor_title_to_desc(self, const1):
             imgpath = "file:///path/to/image.png"
             text = f''
-            text, html, images = markdown_with_inline_image_support(text)
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
             assert images[imgpath].desc == const1
 
+        @pytest.mark.imgproc
         def test_markdown_inline_image_processor_alt_to_desc(self, const1):
             imgpath = "file:///path/to/image.png"
             text = f""
-            text, html, images = markdown_with_inline_image_support(text)
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
             assert images[imgpath].desc == const1
 
+        @pytest.mark.imgproc
         def test_markdown_inline_image_processor_title_over_alt_desc(
             self, const1, const2
         ):
             imgpath = "file:///path/to/image.png"
             text = f''
-            text, html, images = markdown_with_inline_image_support(text)
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
             assert images[imgpath].desc == const2
 
+        @pytest.mark.imgproc
         def test_markdown_inline_image_not_external(self):
             imgpath = "https://path/to/image.png"
             text = f""
-            text, html, images = markdown_with_inline_image_support(text)
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
 
             assert 'src="cid:' not in html
             assert "](cid:" not in text
             assert len(images) == 0
 
+        @pytest.mark.imgproc
         def test_markdown_inline_image_local_file(self):
             imgpath = "/path/to/image.png"
             text = f""
-            text, html, images = markdown_with_inline_image_support(text)
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
 
             for k, v in images.items():
                 assert k == f"file://{imgpath}"
                 break
 
+        @pytest.mark.imgproc
+        def test_markdown_inline_image_expanduser(self):
+            imgpath = pathlib.Path("~/image.png")
+            text = f""
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
+
+            for k, v in images.items():
+                assert k == f"file://{imgpath.expanduser()}"
+                break
+
         @pytest.fixture
         def test_png(self):
             return (
                 "AAAABCAAAAAA6fptVAAAACklEQVQI12P4DwABAQEAG7buVgAA"
             )
 
+        @pytest.mark.imgproc
         def test_markdown_inline_image_processor_base64(self, test_png):
             text = f""
-            text, html, images = markdown_with_inline_image_support(text)
+            text, html, images, mdwn = markdown_with_inline_image_support(text)
 
             assert 'src="cid:' in html
             assert "](cid:" in text
             assert len(images) == 1
             assert test_png in images
 
+        @pytest.mark.converter
         def test_converter_tree_inline_image_base64(
             self, test_png, const1, fake_filewriter
         ):
             assert tree.children[1].path == written[0]
             assert written[1] == request.urlopen(test_png).read()
 
+        @pytest.mark.converter
         def test_converter_tree_inline_image_base64_related_to_html(
             self, test_png, const1, fake_filewriter
         ):
             assert tree.children[1].children[1].path == written[0]
             assert written[1] == request.urlopen(test_png).read()
 
+        @pytest.mark.converter
         def test_converter_tree_inline_image_cid(
             self, const1, fake_filewriter
         ):
             assert tree.children[1].cid != const1
             assert tree.children[1].type != "image"
 
+        @pytest.mark.imgcoll
         def test_inline_image_collection(
             self, test_png, const1, const2, fake_filewriter
         ):
             assert relparts[0].cid == const1
             assert relparts[0].desc.endswith(const2)
 
-        def test_apply_stylesheet(self):
-            if _PYNLINER:
+        if _PYNLINER:
+
+            @pytest.mark.styling
+            def test_apply_stylesheet(self):
                 html = "<p>Hello, world!</p>"
                 css = "p { color:red }"
                 out = apply_styling(html, css)
                 assert 'p style="color' in out
 
-        def test_apply_stylesheet_pygments(self):
-            if _PYGMENTS_CSS:
+            @pytest.mark.styling
+            def test_massage_styling_to_converter(self, string_io, const1):
+                css = "p { color:red }"
+                css_f = StringIO(css)
+                out_f = StringIO()
+                css_applied = []
+
+                def converter(
+                    drafttext,
+                    draftpath,
+                    css,
+                    related_to_html_only,
+                    extensions,
+                    tempdir,
+                ):
+                    css_applied.append(css)
+                    return Part("text", "plain", draftpath, orig=True)
+
+                do_massage(
+                    draft_f=string_io,
+                    draftpath=const1,
+                    cmd_f=out_f,
+                    css_f=css_f,
+                    converter=converter,
+                )
+                assert css_applied[0] == css
+
+            @pytest.mark.converter
+            def test_converter_apply_styles(
+                self, const1, fake_filewriter, monkeypatch
+            ):
+                path = pathlib.Path(const1)
+                text = "Hello, world!"
+                css = "p { color:red }"
+                with monkeypatch.context() as m:
+                    m.setattr(
+                        markdown.Markdown,
+                        "convert",
+                        lambda s, t: f"<p>{t}</p>",
+                    )
+                    convert_markdown_to_html(
+                        text, path, css=css, filewriter_fn=fake_filewriter
+                    )
+                assert "color: red" in fake_filewriter.pop()[1]
+
+        if _PYGMENTS_CSS:
+
+            @pytest.mark.styling
+            def test_apply_stylesheet_pygments(self):
                 html = (
                     f'<div class="{_CODEHILITE_CLASS}">'
                     "<pre>def foo():\n    return</pre></div>"
                 out = apply_styling(html, _PYGMENTS_CSS)
                 assert f'{_CODEHILITE_CLASS}" style="' in out
 
+        @pytest.mark.massage
         def test_mime_tree_relative_within_alternative(
             self, string_io, const1, capsys, mime_tree_related_to_html
         ):
             def converter(
                 drafttext,
                 draftpath,
-                cssfile,
+                css,
                 related_to_html_only,
                 extensions,
                 tempdir,
             assert "send-message" in lines.pop()
             assert len(lines) == 0
 
+        @pytest.mark.massage
         def test_mime_tree_nested_trees_does_not_break_positioning(
             self, string_io, const1, capsys
         ):
             def converter(
                 drafttext,
                 draftpath,
-                cssfile,
+                css,
                 related_to_html_only,
                 extensions,
                 tempdir,
 
             captured = capsys.readouterr()
             lines = captured.out.splitlines()
-            while not "logo.png" in lines.pop():
+            while "logo.png" not in lines.pop():
                 pass
             lines.pop()
             assert "content-id" in lines.pop()
             # follows next must not be another <move-down>
             assert "Logo" in lines.pop()
 
+        @pytest.mark.sig
+        def test_signature_extraction_no_signature(self, const1):
+            assert (const1, None, None) == extract_signature(const1)
+
+        @pytest.mark.sig
+        def test_signature_extraction_just_text(self, const1, const2):
+            origtext, textsig, htmlsig = extract_signature(
+                f"{const1}{EMAIL_SIG_SEP}{const2}"
+            )
+            assert origtext == const1
+            assert textsig == const2
+            assert htmlsig is None
+
+        @pytest.mark.sig
+        def test_signature_extraction_html(self, const1, const2):
+            path = pathlib.Path("somepath")
+            sigconst = "HTML signature from {path} but as a string"
+
+            def filereader_fn(path):
+                return (
+                    f'<div id="signature">{sigconst.format(path=path)}</div>'
+                )
+
+            origtext, textsig, htmlsig = extract_signature(
+                f"{const1}{EMAIL_SIG_SEP}{HTML_SIG_MARKER} {path}\n{const2}",
+                filereader_fn=filereader_fn,
+            )
+            assert origtext == const1
+            assert textsig == const2
+            assert htmlsig == sigconst.format(path=path)
+
+        @pytest.mark.sig
+        def test_signature_extraction_file_not_found(self, const1):
+            path = pathlib.Path("/does/not/exist")
+            with pytest.raises(FileNotFoundError):
+                origtext, textsig, htmlsig = extract_signature(
+                    f"{const1}{EMAIL_SIG_SEP}{HTML_SIG_MARKER}{path}\n{const1}"
+                )
+
+        @pytest.mark.imgproc
+        def test_image_registry(self, const1):
+            reg = ImageRegistry()
+            cid = reg.register(const1)
+            assert "@" 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()
+            reg.register("/some/path")
+            for path in reg:
+                assert path.startswith("file://")
+                break
+
+        @pytest.mark.converter
+        @pytest.mark.sig
+        def test_converter_signature_handling(
+            self, const1, fake_filewriter, monkeypatch
+        ):
+            path = pathlib.Path(const1)
+
+            mailparts = (
+                "This is the mail body\n",
+                f"{EMAIL_SIG_SEP}",
+                "This is a plain-text signature only",
+            )
+
+            def filereader_fn(path):
+                return ""
+
+            with monkeypatch.context() as m:
+                m.setattr(markdown.Markdown, "convert", lambda s, t: t)
+                convert_markdown_to_html(
+                    "".join(mailparts),
+                    path,
+                    filewriter_fn=fake_filewriter,
+                    filereader_fn=filereader_fn,
+                )
+
+            soup = bs4.BeautifulSoup(fake_filewriter.pop()[1], "html.parser")
+            body = soup.body.contents
+
+            assert mailparts[0] in body.pop(0)
+
+            sig = soup.select_one("#signature")
+            assert sig == body.pop(0)
+
+            sep = sig.select_one("span.sig_separator")
+            assert sep == sig.contents[0]
+            assert f"\n{sep.text}\n" == EMAIL_SIG_SEP
+
+            assert mailparts[2] in sig.contents[1]
+
+        @pytest.mark.converter
+        @pytest.mark.sig
+        def test_converter_signature_handling_htmlsig(
+            self, const1, fake_filewriter, monkeypatch
+        ):
+            path = pathlib.Path(const1)
+
+            mailparts = (
+                "This is the mail body",
+                f"{EMAIL_SIG_SEP}",
+                f"{HTML_SIG_MARKER}{path}\n",
+                "This is the plain-text version",
+            )
+
+            htmlsig = "HTML Signature from {path}"
+
+            def filereader_fn(path):
+                return f'<div id="signature">{htmlsig.format(path=path)}</div>'
+
+            def mdwn_fn(t):
+                return t.upper()
+
+            with monkeypatch.context() as m:
+                m.setattr(
+                    markdown.Markdown, "convert", lambda s, t: mdwn_fn(t)
+                )
+                convert_markdown_to_html(
+                    "".join(mailparts),
+                    path,
+                    filewriter_fn=fake_filewriter,
+                    filereader_fn=filereader_fn,
+                )
+
+            soup = bs4.BeautifulSoup(fake_filewriter.pop()[1], "html.parser")
+            sig = soup.select_one("#signature")
+            sig.span.extract()
+
+            assert HTML_SIG_MARKER not in sig.text
+            assert htmlsig.format(path=path) == sig.text.strip()
+
+            plaintext = fake_filewriter.pop()[1]
+            assert plaintext.endswith(EMAIL_SIG_SEP + mailparts[-1])
+
+        @pytest.mark.converter
+        @pytest.mark.sig
+        def test_converter_signature_handling_htmlsig_with_image(
+            self, const1, fake_filewriter, monkeypatch, test_png
+        ):
+            path = pathlib.Path(const1)
+
+            mailparts = (
+                "This is the mail body",
+                f"{EMAIL_SIG_SEP}",
+                f"{HTML_SIG_MARKER}{path}\n",
+                "This is the plain-text version",
+            )
+
+            htmlsig = (
+                "HTML Signature from {path} with image\n"
+                f'<img src="{test_png}">\n'
+            )
+
+            def filereader_fn(path):
+                return f'<div id="signature">{htmlsig.format(path=path)}</div>'
+
+            def mdwn_fn(t):
+                return t.upper()
+
+            with monkeypatch.context() as m:
+                m.setattr(
+                    markdown.Markdown, "convert", lambda s, t: mdwn_fn(t)
+                )
+                convert_markdown_to_html(
+                    "".join(mailparts),
+                    path,
+                    filewriter_fn=fake_filewriter,
+                    filereader_fn=filereader_fn,
+                )
+
+            assert fake_filewriter.pop()[0].suffix == ".png"
+
+            soup = bs4.BeautifulSoup(fake_filewriter.pop()[1], "html.parser")
+            assert soup.img.attrs["src"].startswith("cid:")
+
+        @pytest.mark.converter
+        @pytest.mark.sig
+        def test_converter_signature_handling_textsig_with_image(
+            self, const1, fake_filewriter, test_png
+        ):
+            mailparts = (
+                "This is the mail body",
+                f"{EMAIL_SIG_SEP}",
+                "This is the plain-text version with image\n",
+                f"",
+
+            )
+            tree = convert_markdown_to_html
+                "".join(mailparts),
+                pathlib.Path(const1),
+                filewriter_fn=fake_filewriter,
+            )
+
+            assert tree.subtype == "relative"
+            assert tree.children[0].subtype == "alternative"
+            assert tree.children[1].subtype == "png"
+            written = fake_filewriter.pop()
+            assert tree.children[1].path == written[0]
+            assert written[1] == request.urlopen(test_png).read()
+
+        def test_converter_attribution_to_admonition(self, fake_filewriter):
+
+
 except ImportError:
     pass