Tying up a loose end - How class-based emails will save your day
Published July 11, 2024
This video features Ronny Vedrilla at Django Day Copenhagen 2023 in Copenhagen, Denmark.
"Tying up a loose end - How class-based emails will save your day" by Ronny Vedrilla at Django Day Copenhagen 2023. Talk description at: https://2023.djangoday.dk/talks/ronny/
Ronny Vedrilla built Django Pony Express to make email code in Django less repetitive and easier to maintain. Its class-based email services let projects define shared defaults—such as template context, translations, headers, and subject prefixes—once, then customize each email; it can also generate plain text from HTML and supports validation, attachments, and email factories. The package includes test helpers for finding sent messages and checking their recipients and content, including both HTML and plain-text bodies. Logging and background sending are not currently built in, though projects can add them through their base service; the speaker argues that the package fills a gap Django’s built-in email tools leave open.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
Speaker 1: Welcome on stage, Ronnie. So class-based views? Anyone? Yeah?
Speaker 2: Class-based emails?
Speaker 1: There is also class-based emails now.
Speaker 2: Yeah. Okay, um my name is Ronnie and uh today I want to uh talk about about a package I've built about a year ago uh called Django Pony Express. And um this is my very first time on stage with a proper talk so please cut me a little bit slack. if something goes sideways. Okay, this is what's going to happen. I'm going to introduce myself. I motivate why I think this topic is important. We have an extensive history lesson. Just kidding. Um a status quo, if you look at the Django docs, um then the actual solution, and at the end some critical review. All right, that's me.
Speaker 2: Um, Ronnie. I'm working with Django since 2012. This means version 1. 4. It's quite agent. Since 2019 I'm a tech evangelist, which means I have time, for example, to build packages like this Since recently, I'm a member of the DSF, Django Software Foundation, and I'm the organizer of the Django Meetup Cologne, which is the only Django meetup left in Germany as far as I know. And uh if you want to get in touch, please do via LinkedIn. This is the only social media I have, uh, so it has to do. Um I work at a company called Ambient, we're in Cologne and we use uh Django and React. js, if we can help it. And uh they're giving me the opportunity to be here. So um yeah, I'm very happy for this.
Speaker 2: Okay, motivation. Um why is creating and maintaining emails such a pain with Django? This is a thing that I heard myself say a lot and from others. And um okay, so emailing, why is this important? Um many business applications rely heavily on emailing, even so even more if you have end customers on your platform, if it's not just an internal application Some people might say, okay, but we have like fancy new things like in-app notifications. But the problem with in-app notifications is that at first Maybe they're not legally binding enough for your case. You have to have an email. What do you do if users don't read them? If they don't go to your platform, well you have to send them an email about, hey, here are some notifications. Please read them.
Speaker 2: So in the end, emails are a quite important part for most, many, maybe all business applications, at least in my case. Um since um the last eleven years ish, I've worked on about twenty Django applications more or less. And the things that I found most tedious about working with emails or implementing emails were basically always the same You would have an HTML part and a plaintext part. The plain text part is just for legacy reasons and it's kind of the protocol, you have to have it. But it's not it's not dry. It means you have two templates, you have to maintain them separately Um you would usually have an email template, like a base template, which makes you your emails look the same, like different in
Speaker 2: different content, but um Yeah. And you have to have global variables. For example, the I don't know the unsubscribe link or some some URL where your images come from or your company logo or something like this. And um I would you have I had to set them per email um and I would tend to forget one of them and then you know the email was broken and it was very annoying. Emails for kind of like an external API, even if the code was in my code base. Um and therefore they weren't unit tested. I mean emailing is kind of an external API, I come to this later, but not having tests as we have heard from Will. um a few minutes ago uh is not so nice, so this would lead to many bugs. And I would find myself build convenience features like um the unsubscribed link or setting some headers in every project again and again and again.
Speaker 2: And this was again tedious. So um why is the package called Pony Express Django Pony Express? Um the Pony Express was a super fast mailing service in the US in uh 1960. It only lasted for one year because then there was a telegraph. Bad luck for them. Nevertheless. Nevertheless, um they used ponies for this and we all know the Django pony and it was about emailing or mailing, not emailing. Um so I think the name was kind of Fitting. And here's a picture of me with some donkeys. The local zoo in Cologne doesn't have any ponies, so donkeys have to suffice.
Speaker 2: Um okay so if you're starting um or working on a um on a Django application and you want to do emailing, what do you do usually? You go to the really great Django documentation. And then you find we'll find more or less this. Um it's a function, it will send an email, um, it's very simple. Um, it shows you how to set parameters, they are set directly. Um but unfortunately if in my real life cases um the stuff would be way way more complicated and it would more like this. Um I don't know if you can read it back there, but you know the details are not so important. The thing is that this would be just one of many, many emails I would have in this whole function. And this contains um the from, the to, internationalization, um
Speaker 2: the base variable. Let me see if I have a pointer, nice. Um here are these base variables for the template. Then I have here like the two uh two templates I have to um I have to define. Then I have to know about how to create this multi-part email, like having this HTML part. Maybe I don't want to really know or need to know that this is the email multi-alternatives class, maybe just, you know, one Django that it works, right? Um there would be some kind of logging. Um Then there would be some error handling. What if something goes sideways? It's still an external API. It's an email provider. Yeah, maybe it does you know has a timeout or something. And I don't even mention the headers in this example So this is a lot of stuff I would find myself implementing over and over again.
Speaker 2: And um this is the drawbacks. At first, if I have lots of redundant code, it's not dry. So yeah, this is annoying and has a lot of uh lot of drawbacks. It's very easy to make mistakes and I tend to make them over and over again As I said before, the duplicate templates for HTML and plain text are really annoying because in most cases I wouldn't care about the plain text because nobody's looking at plain text anyway. The unencapsulated email API that I have to know which class to use from Django to send an HTML email. I can't inherit anything. I like object orientation. And every log message would look a little bit different depending on who and when it was built, meaning if I want to do some
Speaker 2: aggregation from my logs, it was very tedious because everything looked a little bit different and I would find myself cleaning this up over and over again. So I'm going to take a zip. This is um the solution now. That I've built. And we use this in a couple of projects, and this is the feedback I get from my colleagues. It's quite neat. So let me show you why they think that If you're interested in the package, here's a quite huge QR code which leads you to PyPy. If you're interested, you can you know check it out there.
Speaker 2: So, class-based emails. As you probably know, we have uh class-based views in Django and function-based views in Django. It's kind of the the thing that inspired me to go from this function based use we've seen in documentation before, like function based emails to class based emails. That means that you have a base email service and um you extend your email instances from this and this will provide Most, maybe all of the stuff you need, depending on what what your uh requirements are, you get it out of the box. Um you stay dry, you only override what you want to change. Uh since Wednesday it's Python 3. 12 compatible. I made sure to get this done before I'm here. And um obviously docs where it didn't happen. Um it's fully documented at Read
Speaker 2: the Docs. So if you want to you know read up anything or want to know what it's capable of, you can do this there. So, um if you remember this huge abomination code email thingy I showed you a couple of emails ago. Uh a couple of emails, nice, couple of slides ago. Um this is basically doing exactly the same. Um you see there is no code duplication, it's very very uh less code, it's testable. I come to this later. Um It's super fast to create new emails because it's just a couple of lines and you can just, you know, copy them, adjust a few things, and you're good to go. Um it's very neat. And the thing you need to uh to take care of is that here we don't have an email-based service, but it's in my case, it's the Beer Bear Incorporated.
Speaker 2: We have the Beer Bear-based email service. So all the stuff that you have to have for your project, the variables for your base template, some customizations for logging or internationalization would go into this base class. So you would extend the base class and put the stuff there. So still you have to do it one time per project, but not one time per email you're creating. And um here's an example. I'm sorry it's quite small, but No, I will go over the details. This service here will encapsulate, uh will extend from the base email service, which comes from the package. Um, you can set some headers. And then you have a function getContextData. If you used uh used to Django class-based views, you should this should look familiar.
Speaker 2: And this is basically where you can set the variables for the base template. And this is a thing that's More or less I have in every project with a little bit more or a little bit less configuration. Yeah, and every email instance you're creating will extend from this custom one. Okay, then a couple of features. Um, logging in my up in my experience is a very individual topic, and um It's uh logging is not part of the of the Pony Express package yet because in all the projects I had to work, it was a little bit different. Um and so I just decided to leave it out for now. But as you've seen the My Base class before, you can just add a couple of things.
Speaker 2: This is quite an extensive example just to show you what's what's possible in theory. This could be easier and just add the logging in the methods you need to you need to override and you know customize it to your style. Still, you have to do it, but you just have to do it once. Per project, so you just get it over with in half an hour and you're good to go. Another thing is internationalization. Um, out of the box it works and it will take the the defaults you set in your Django settings. But for example, um here in this um here on the right you see that maybe uh the language is not set by the request or by the Django settings, but maybe it's an attribute of the user, and the user can decide in which language he wants to have uh wants to have his emails or his content in general. So you can just override this method, get translation, and then do basically all the magic you want to do.
Speaker 2: You have the recipient, you have the context data, it's a class, right? You can do whatever you want. Um then is another thing I call the ri reply to dilemma. Um usually if you send emails They will send from some email address called no reply at your domain. com. And this is good because it's not an actual person sending this, but it's just some kind of bot or email service or whatever AWS S E S. But sometimes people want to respond to this or have to respond to this. Imagine, for example, I don't know, somebody gets an email and he thinks it's not applicable for him, or maybe he's confused and he wants to reply. And um that's really nice if you set the reply to header to some email address or email account that's actually you know
Speaker 2: read by a human human being. That people can they know okay this wasn't sent to a person, but still if they want to hit the reply button, you know the email goes somewhere and the person can can deal with the issue. Um then some more stuff if you need more convincing. This is a good idea. Um it has under the hood an HTML to text conversion. Um so this means you just have to provide the HTML template, you actually care about and it will render the the plaintext part. In some cases you care for the plaintext part. For example if you have a very I don't know sophisticated fancy system If you provide a plaintext template, then this will be taken, it
Speaker 2: will not be rendered automatically, so you're good to go. But the default is you know it's easy the solution that's more easy and uh convenient Um then as you have might seen in my my first example of this custom base class, um you can set a subject prefix and this is a thing that's really nice. So it will set a prefix to all of the email subjects. In my case, because the Beer Bear Incorporated will set Beer Bear Incorporated, hyphen, and then for example account created or account. account closed or new transaction or whatever. And this will help the uh the end user who actually gets the email in his email client to uh to filter by them to make kind of rules to sort them in some and something and everything will look very uniform and neat and more professional in the end.
Speaker 2: If you want to have some custom validation to ensure that, you know, if this email should really be sent, for example, I don't know, the recipient needs to have some conditions, I don't know, the recipient needs to be active. Yeah, if if for example the user can be inactive. You can put this there in a in a in a method. You can uh change the error handling, for example, if you want to override if this should fail silently or noisily. Um and attachments are possible of course as well if you want to attach files. Okay, um this is the thing, it's a little bit more complicated. I won't go to into too much depth. Um maybe you know about um
Speaker 2: you know about factories from for example factory boy as uh we'll pointed out In this case, imagine you have an email and you have a business application and you want to inform all of your managers how their employees or their divisions are doing. And basically the email looks the same, but the content differs slightly or a lot per user. And sure you can just create an email and you know build some kind of loop and inject the proper data per user, but if you don't want this, you can use the factories. Here's a very simple example. Just note that here you're basically setting the this is a thing you've seen before, it's just an email instance class. You just set this here in the factory, and this will take care of the looping and everything. If you want to know more about this, just check out read the
Speaker 2: docs because it's a little bit complicated topic and some people might not need this at all. So this was very nice, but I think the best the greatest benefit kind of fits to this day because we had so many test topics. Uh, is we can properly unit test our emails, finally. If you have a unit test and you execute some code that sends an email, Django automatically will put this In the mail. outbooks object. It's just a thing, it's lying around, it's basically a list, and this will contain your email. And if you want to, for example, have one email in there and you want to check the content, the HTML content You have to do mail dotbox on position zero dot alternatives on position zero on position zero, which is not really intuitive and I had to look it up all the time.
Speaker 2: It's very annoying. So there is an email test service, and you can just put it in your setup test data. You just need per test, so setup test data. Um and put it here in a variable, email test service, and then with this, this basically provides everything you need for testing your emails. And my idea behind this is That I turn this mail outbox to something that looks like a Django query set because there's it's it's a list, like for example, a database table. Uh you know what I mean. Um and you can do a filter on this, you can search, you can maybe say, Hey, I want the first object, I want the last object, I want to know how many objects are in there, like a count, like the the regular things you would do with um Django query sets. So, how does it look like?
Speaker 2: Um, here is a quite typical test case I would have. It's sorry, it's again it's a little bit small. Um But let's just go go quickly through it. At first you would just say I have my email test service and that filter for subject of the subject I'm interested in. For example, the email with account created. And I count how many I have there, then I would say, okay, please now send trigger this code that will send the email. Then I will check again. For emails with a subject, and then I would assert that I have one email more because I just sent the email. If it's not one email more, then obviously something went sideways. And then I can do a list of emails. first, and this gives me a mail object. This mail object has a bunch of attributes you would expect, like the headers, like CC2, reply to and etc. So you can do it in a cert in
Speaker 2: because Cos two is like always a list. There can be more multiple recipients. You can check if the email has the right um, yeah, if it if it was sent to the right address. And this is I think the best part, there's a helper called a third body contains because we have to have the plain text part. So we will always have, even though we don't set it explicitly, we will always have um two parts of of content and some people still might you know look at the at the plaintext part so we have to ensure that it's correct. And what it does is You wouldn't, in my at least the way I use it, you wouldn't compare the whole template, but you would just look for certain key elements. For example, it's like the the um Um the salutation set, if the is the title correct, is as I said, some certain keywords were would be in there.
Speaker 2: And this will automatically check for you the HTML part and the plaintext. part and would just complain, hey, here something is off. Um if something is off. And this is this saves a lot of time. And it uh um It uh assures that you don't forget something because as I said, if I go one slide back, doing this all the time is quite annoying and it's really hard to tell uh the juniors to, you know. How this works. Um, there's a little bit more stuff in the documentation again, but this is like the general idea, how it how it works. So, Outlook. Sending emails is obviously you need an external API for this, some kind of email provider.
Speaker 2: Whatever that might be. Um so this shouldn't happen in the main thread. And this is a very common thing that people need to know, need to think about So um I was thinking of uh adding some functionality um maybe with an integration for Django Q or Celery or just Python threading to basically by default not send the emails in the main thread and just to You know, that you don't have to think about this every time, but it just um uh yeah, that it's um yeah, not sent in the main thread. So you don't have to think about it. As I said, the logging thing is not part of this yet. I think I will add in the future some basic functionality for this that you don't have to worry about this. Because in the end, logging is just a plus and you can just display And with a big wink, maybe a pull request to Django Core.
Speaker 2: So, um last slide. Um so a critical review. Um there's like a big discussion going on uh about people saying Django class-based views. Are not so nice and you shouldn't use them. Function-based views are superior. So why should I put my money on class-based emails? Well, obviously, you could write a function-based wrapper for this. But you have to do it yourself and everything here is already there. So I think it's quite convenient to just stick with this. Then some people will say, hey, if I implement like all my 200 emails with this, what if the package becomes discontinued? As always, you can just raise a PR. Everything is at GitHub and open source, obviously. Um you can always fork the package or you know
Speaker 2: just copy the important parts for you into your project and maintain them themselves, uh yourselves. So um Nothing critical there. And um then the last thing, if this is really like kind of closing a gap with Django, Um why do we need a package for this? Why isn't this part of Django? Well, maybe it should, but it isn't, so I built it. Thank you
Speaker 1: Thank you. Do we have any questions? Yes. Oh, one, two.
Speaker 2: I'm sorry I don't have candy. Oh you were personal. I don't know. I could see that you were personal. Oh then I was first.
Speaker 3: I'm slightly disappointed by the lack of cancer. I'm reconsidering my question. No, um, there's cake next, I heard. Ooh. Um you say that You sort of suggest that most people don't care about plain plain text emails, which I think in Western Europe is true and the rest of the world is some people do care about it. So my question is, how do you deal with hyperlinks? from HTML in the plain text version of the um email because of course you cannot click uh a hyperlink in a plaintext email but if you only show the label and not the href from the from the anchor element. There is no link. So how do you deal with that?
Speaker 2: Um well actually I don't. I have a package for this. Um this is a dependency. I think it's called HTML2Text. Um it's quite basic, but it does the job. And this will render um it has some kind of rules how to render things. For example, if you have a table or something, it will just um Make a pipe. This looks sometimes a little bit odd depending on your base template. And it will render links. And then if you put this to any modern uh email client, then this will be automatically interpreted again as a link, so you can still click it.
Speaker 3: Cool. Awesome. Thank you. And cool presentation, by the way, even though there was no candy.
Speaker 4: Thank you. I use plain text email, so thank you for generating it. Um it seems like this would be useful outside of Django as well. Most of it, I like I guess you're using templates, but aside from that, probably a lot of this could be used outside of Django. What do you think? And if I want to use this outside of Django, what should I do?
Speaker 2: That's a really good question. Never thought about this before. Um well it hooks into some of the uh of the Django framework I would say, like for example, how to get the translation or the internationalization, the language and stuff like this. But um if you want to use this in another scope, I don't know for your FLAS project or whatever, I think you just can copy, you know, the the um the source code and change a few things because it's you know it's just it's not so much code in the end, right? It's just a base class and some stuff that's Django specific. But it should be possible to, you know, just you know Get the code and change a few things and then make this uh yeah work in other frameworks or even just you know
Speaker 2: without frameworks
Speaker 1: Any more questions? Questions from the internet? I I have a question. Uh so I think I've seen and I I don't want to say something that's uh incorrect on stage but in the Django core I think I've seen that uh uh in the um Django Contrib auth there is a bunch of emailing going on as like uh part of the views like for um and I've I've somehow picked up a pattern because I've been using class based emails kind of in my own way, like copy pasting it from project to project. It's really nice to have a but formalized by the way. Um but then I picked up like using templates for the subject. Um so so you use like a template file.
Speaker 2: Okay.
Speaker 1: Uh in order to generate the subject and then you can use context variables inside the
Speaker 2: interesting.
Speaker 1: Um have uh you haven't met that.
Speaker 2: Well um I go a different approach. I have a method which is called getSubject and you can override this and then basic this is basically the part where this subject prefix is is glued together with the uh with the real subject of of the email instance. And you can override this per email and then do this I mean you can just do a render to string thing and then render a tra a template if you want. possible.
Speaker 1: Ah, thank you. I knew that it w feels so wrong to you say to this. Thank you. All right.
Speaker 2: But I think it's not I I can't think of a reason why you want to do this because in the end it will there won't be anything fancy in there, right? It's just text You can't have like long text and everything. So I think having this method you can override should suffice in 99. 9 % of the cases. But you know, if you want to use a template, feel free to do so.
Speaker 1: Um so uh thank you so much to Ronnie.
Speaker 2: Thank you.
Speaker 1: Um
Typical implementations repeat setup for each email, maintain separate HTML and plain-text templates, and can forget shared context or headers. The repeated code is error-prone and makes consistent logging and testing harder.
Discussed at 2:42You create a project-specific base email class for shared settings and behavior, then define each email by extending it and overriding only what differs. The package handles common work such as rendering multipart emails, with options for validation, attachments, subject prefixes, and reply-to headers.
Discussed at 8:08Its email test service provides query-like filtering and access to sent messages, so tests can check counts, recipients, and other attributes. A helper can check that expected text appears in both the HTML and plain-text bodies.
Discussed at 15:53It uses an HTML-to-text dependency that converts links into text the email client can recognize as clickable. The speaker identifies the dependency as HTML2Text.
Discussed at 22:54The speaker says it relies on some Django-specific features, such as translation, but suggests copying the relatively small source code and adapting those parts for another framework or no framework.
Discussed at 23:57Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.
Published October 13, 2024
Published October 13, 2024
Published October 13, 2024
Published October 13, 2024
Published October 13, 2024
Published October 13, 2024