render_shell()

The migration path for mails whose body already exists — a notification assembled by string concatenation, say — and which nobody wants to re-author as a kit template. The shell contributes the whole document; your body contributes its own markup and nothing else about it changes.

from imio.emailkit import render_shell

html, text = render_shell("Point 'Budget 2026' : état modifié", legacy_body_html)

It is a render() sibling, not a builder method — it returns the same (html, text) pair and shares render()'s code path, so Email sends its output with no change at all.

Signature

  • Name
    subject
    Type
    msgid or str
    Description

    An i18n msgid or a literal string, exactly like .subject(). Translated into the render language and handed to the shell as its heading, under the name subject.

  • Name
    body_html
    Type
    str
    Description

    The existing body, inserted verbatim into the shell's body_html slot. Not sanitised, not reformatted.

  • Name
    language
    Type
    str, optional
    Description

    As render() — the negotiated language when omitted.

Raises EmailkitError if the compiled shell is absent from the egg.

What the shell contributes

The inlined CSS, the accessibility defaults, lang on <html>, the header and footer, the theme tokens and the dark-mode hints. Your body goes in byte-for-byte.

The plaintext part

Always a naive text extraction of the rendered HTML, and not warned about.

A hand-authored shell.txt.pt twin could not exist even in principle: its only content would be body_html, which is HTML, so the twin would put tags in the plaintext part. This is the designed path here, not the missing-twin fallback that render() logs a deprecation for.

No preheader

The hidden preheader div collapses, and that is the shell's decision rather than an omission: spending the whole inbox snippet repeating the subject line the client already shows is worse than letting the client continue the snippet into the legacy body.

The runtime path in the layout still exists, so nothing here forecloses a future preheader argument.

One thing to check in your legacy body

Everything else about a legacy body survives: inline styles, bgcolor, nested tables, cellpadding, entities, unclosed tags, even a whole pasted <html> document. None of it raises — but a pasted full document does produce a nested <html>/<body>, which is invalid HTML that clients tolerate rather than something to rely on.

You usually want the builder instead

Email can carry a legacy body too, because the kit layout defines the body_html slot for every template and not just the shell:

Email("imio.emailkit:notification").to(member).with_context(
    title=subject, body_html=legacy_body_html
).send()

Prefer this when you can. You get per-language sending, recipient adapters, attachments and transaction-safe delivery, and you keep the registration's subject, its preheader and its hand-authored plaintext twin — none of which render_shell has.