Support for HTML Bodies in Auth.send

96 views
Skip to first unread message

Hadi Zarkoob

unread,
Nov 21, 2025, 11:08:57 PM11/21/25
to py4web
Hi all!

The default body of Auth messages is a plain string. However, it’s common to send HTML bodies as well. 

The current send method in Auth assumes the body is always a string. It would be helpful to update this method so it can also handle bodies that include an HTML component. The required changes are minimal — we just need to allow the body to be either a 2-list or 2-tuple, in addition to a single string.  

This would be an example:

auth.param.messages['verify_email']['body'] = (VERIFY_EMAIL_PLAIN_TEXT, render(
    filename=os.path.join(CURRENT_FILE_FOLDER, "templates/emails/verify_email.html")))

Thanks!
Hadi

Massimo DiPierro

unread,
Nov 21, 2025, 11:27:45 PM11/21/25
to Hadi Zarkoob, py4web
Good idea. 
Could we just detect that is starts with <html>?
We already do that in auth.mailer.send so I thought this was already possible

--
You received this message because you are subscribed to the Google Groups "py4web" group.
To unsubscribe from this group and stop receiving emails from it, send an email to py4web+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/py4web/b78ccf9a-4f16-4ae9-9d81-1ff387ef1b42n%40googlegroups.com.

Hadi Zarkoob

unread,
Nov 21, 2025, 11:45:50 PM11/21/25
to py4web

Thank you, Massimo. I’m not able to find auth.mailer.send. Did you perhaps mean auth.sender.send?

It looks to me like the key change is to check whether body is a 2-element list or tuple and, if so, apply format(**d) to both elements. I was able to get it working by simply updating this line as follows:

        body = message["body"].format(**d)

to:

    if isinstance(message["body"].text, (list, tuple)):
        body = [element.format(**d) for element in message["body"].text]
    else:
        body = message["body"].format(**d)

The rest is handled by the Mailer library.

Please let me know if any additional information is needed.

Thank you!

Hadi

Massimo DiPierro

unread,
Dec 7, 2025, 9:20:44 PM12/7/25
to py4web
Sorry for the delay, this is merged. Please help me check it.

Hadi Zarkoob

unread,
Dec 9, 2025, 12:47:40 AM12/9/25
to py4web
Thank you for this!

Looks great, I just think it should be  isinstance(message["body"].text, (list, tuple)) instead of  isinstance(message["body"], (list, tuple)) because message["body"] is of the type pluralize.lazyT not tuple. Does that make sense?

Hadi

Massimo DiPierro

unread,
Dec 9, 2025, 2:34:11 AM12/9/25
to py4web
Can you show me an example of how you are using internationalization for the message body? That .text should not be required but I may be missing something

Hadi Zarkoob

unread,
Dec 9, 2025, 3:03:13 AM12/9/25
to py4web
I do not use internationalization. It is done, by default, inside auth.py.

Without .text  isinstance(message["body"], (list, tuple)) does not return true even when the user sets the message["body"] to a tuple. The code inside auth.py changes the type from tuple to plurazie.lazyT.

Please let me know if more information is needed.

Thank you,
Hadi

Massimo DiPierro

unread,
Dec 9, 2025, 3:18:00 AM12/9/25
to Hadi Zarkoob, py4web
You are right. Now I understand the problem better and I think the solution is a bit different.
I added a commit to master. Can you help me test it?



Message has been deleted
Message has been deleted

Hadi Zarkoob

unread,
Dec 9, 2025, 7:58:37 PM12/9/25
to py4web
I just tested it, and everything works great!

I'm glad to see that py4web is becoming quite stable.  

- Hadi

Massimo DiPierro

unread,
Dec 10, 2025, 2:07:36 AM12/10/25
to py4web
thank you. releasing 1.20251209.1 with this change

Hadi Zarkoob

unread,
Jan 15, 2026, 12:42:52 AM (7 days ago) Jan 15
to py4web
Hi Massimo,

I’ve realized that my previous test was invalid because I hadn’t restarted py4web after making the update.

Further testing indicates that there are two issues with the current implementation:

  1. The output on this line is of type map, but later on we check for the types tuple and list.

  2. The send method in the Mailer library expects a string, but we are passing lazyT objects.

The following change to this section of auth.py resolves both issues. However, I’m not sure whether this is the best approach, or whether it might introduce unintended side effects elsewhere in the system that I’m not aware of.


                if isinstance(value, (list, tuple)):
                    group[key] = map(T, value)
                else:
                    group[key] = T(value)  

===>

                if isinstance(value, (list, tuple)):
                    group[key] = [T(x).text for x in value]
                else:
                    group[key] = T(value).text  

Thank you!

Hadi

Massimo DiPierro

unread,
Jan 17, 2026, 10:52:17 PM (4 days ago) Jan 17
to py4web
Pushed a slight different implementation. I think it should be str(T(value)) because T(value).text is the same as value. Please let me know if that works for you.

Hadi Zarkoob

unread,
Jan 18, 2026, 2:16:37 AM (4 days ago) Jan 18
to py4web
Thank you, Massimo. 

I gave it a try and realized there remains an issue with the new implementation: str(T(value)) returns an error when value includes format (replacement) fields such as first_name or link.

I tried applying str in the send method in auth.py, and it seems to resolve the issue.

An alternative solution is to apply the str function in the Mailer library. Currently, I believe the library applies str to email bodies with a single element but not double elements.

Please do let me know if more information is needed.

Thank you,
Hadi

Massimo DiPierro

unread,
Jan 18, 2026, 11:54:52 AM (3 days ago) Jan 18
to Hadi Zarkoob, py4web
I am still a bit confused but this but I posted another possible fix. Can you try it? Thanks for the realtime help.

Hadi Zarkoob

unread,
Jan 18, 2026, 4:47:08 PM (3 days ago) Jan 18
to py4web
Thanks, Massimo, for the recent changes. They seem very helpful to me and in the right direction.

My testing indicates there are two more minor points to address:

1. There seems to be a typo in this line.

text = to_bytes(body, encoding)    ===>    text = to_bytes(text, encoding) 

2. The following lines in mailer.py return an error if the subject is of the type LazyT. A quick fix is adding str to this line of auth.py. I'm not sure if this is the cleanest solution, though.

  File "/py4web/py4web/utils/mailer.py", line 744, in send
    payload["Subject"] = encoded_or_raw(to_unicode(subject, encoding))

  File "/py4web/py4web/utils/mailer.py", line 71, in to_unicode
    text = text.read()

AttributeError: 'lazyT' object has no attribute 'read'

BTW, here is a low-priority enhancement suggestion. mailer.py currently treats single-element bodies with a bit more care than two-element bodies. I am referring to these lines. (They check if the body is of the type bytes. If so, they decode it. If not, and the input is not of the type str, they force it to be of the type str.) We may want to apply these checks even to the bodies with two elements. Of note, if we do so, we no longer need to use str functions in this section of auth.py, and more generally, mailer.py becomes a more robust library.

Thank you!
Hadi

Massimo DiPierro

unread,
Jan 18, 2026, 8:50:35 PM (3 days ago) Jan 18
to py4web
OK. Fixed 1 as you recommended.
About 2 I also took the route you recommended and I agree it is for the best. But as before, I will rely on you for testing. ;-)

Massimo

Message has been deleted
Message has been deleted

Hadi Zarkoob

unread,
Jan 19, 2026, 2:43:16 AM (3 days ago) Jan 19
to py4web
Massimo, 

I believe I've found a minor issue and a major one.

- Minor: Shouldn't we replace body with html is this section of the code?

- Major: The code still does not work if the body is of the type lazyT. In that case, the code gets caught in this section

I am not well-versed on mailer.py as a whole, but I believe the following logic should be used in the to_unicode function:

def to_unicode(text, encoding="utf8"):
    """Converts a body (str, bytes, file-like) to unicode"""
    if text is None:
        text = ""
    elif not isinstance(text, (str, bytes)):
         # if the text is not a string or bytes, maybe it is a file
         text = read_or_stringify(text)
    elif isinstance(text, bytes):
        text = text.decode(encoding)
    return text 


where  read_or_stringify is defined as:

def read_or_stringify(text):
    """
    Return text.read() if available, otherwise fallback to str(text).
    """
    try:
        return text.read()
    except Exception:
            return str(text)

Hadi

Massimo DiPierro

unread,
Jan 19, 2026, 10:58:48 AM (2 days ago) Jan 19
to py4web
You are right about the logic in "major". I implemented the fix in a slightly different manner but I think it will accomplish what you suggested.
I do not think we need to assign html here because it is done just below if indeed the body contains "<html...</html>".

Hadi Zarkoob

unread,
Jan 19, 2026, 5:29:19 PM (2 days ago) Jan 19
to py4web
Thank you, Massimo. That was a clean implementation—I learned something new!

I’ll test it shortly, but I have three quick questions for now:

  1. Can to_unicode() accept a list as input? This question is prompted by this line.

  2. The method description suggests that the HTML portion of the body list can be provided as a file, but I don’t see which part of the code handles this.

  3. Should we turn this "if" into an "elif"? It seems to me that there is no advantage in checking anything else if text is None.

Thank you!
Hadi

Massimo DiPierro

unread,
Jan 19, 2026, 11:21:31 PM (2 days ago) Jan 19
to py4web
First of all: I apologize. It is very bad practice for me to commit to master and ask you to review it. We should instead be working on a branch and discuss on a ticket. :-)
Anyway, given that this code is and always was broken on master will continue the bad practice until it is fixed. For the next issue I will act more professionally.

I addressed 1, 2 in the last commit
3. I do not think it should be an elif because we do not know if text.readI() returns bytes or str.

Thanks again for your help.

Hadi Zarkoob

unread,
Jan 20, 2026, 6:00:57 PM (yesterday) Jan 20
to py4web
No worries at all. Everything looks perfect and polished to me, and all my tests pass.

I really appreciated how efficient our message exchange was.  

Massimo DiPierro

unread,
Jan 20, 2026, 11:00:19 PM (20 hours ago) Jan 20
to py4web
Thank you for all your help!
Reply all
Reply to author
Forward
0 new messages