django-email-sender 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Egbie Uku
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ # Include the README files
2
+ include README.md
3
+ include usage.md
4
+
5
+ # Include the email templates folder
6
+ recursive-include email_templates *
7
+
8
+ # Include all Python files
9
+ recursive-include emailsender *.py
10
+
11
+ # Include any other necessary files (e.g., LICENSE, etc.)
12
+ include LICENSE
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-email-sender
3
+ Version: 1.0.0
4
+ Summary: A chainable Django email sender utility.
5
+ Home-page: https://github.com/EgbieAndersonUku1/django-email-sender
6
+ Author: Egbie Uku
7
+ Author-email: egbieuku@hotmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Framework :: Django
10
+ Classifier: Framework :: Django :: 3.2
11
+ Classifier: Framework :: Django :: 4.0
12
+ Classifier: Framework :: Django :: 5.0
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.7
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENCE
18
+ Requires-Dist: Django<6.0,>=3.2
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # πŸ“§ Django Email Sender Utility
31
+
32
+ A clean, reusable, lightweight and chainable utility class for sending emails in Django using templates. It supports both HTML and plain text templates, context injection, and flexible usage β€” either directly, via subclassing, or abstracted into functions.
33
+
34
+
35
+ 🧠 Why Use This?
36
+
37
+ While Django already provides a way to send emails, it can become verbose and repetitive. EmailSender abstracts the boilerplate and lets you send templated emails fluently.
38
+
39
+
40
+ ## ✨ Features
41
+
42
+ - Chainable API (`.to()`, `.from_address()`, etc.)
43
+ - Supports HTML and plain text templates
44
+ - Uses Django's template system for dynamic content
45
+ - Easy to integrate and override
46
+ - Encourages clean code and reusability
47
+ - Supports subclassing or functional abstractions
48
+
49
+ ---
50
+
51
+
52
+ ---
53
+
54
+ πŸ›  Available Methods
55
+
56
+ Method Description
57
+ ```
58
+
59
+ - create() # Class factory method to instantiate the EmailSender.
60
+ - from_address(email) # Sets the sender’s email address.
61
+ - to(recipients) # Accepts a string or list of recipient email addresses.
62
+ - with_subject(subject) # Sets the email subject.
63
+ - with_context(context) # Context dictionary used in the templates.
64
+ - with_text_template(path) # Path to the plain text email template.
65
+ - with_html_template(path) # Path to the HTML email template.
66
+ - with_headers(headers) # Optional custom headers as a dictionary.
67
+ - send() # Sends the email. Returns the number of successfully delivered messages.
68
+ ```
69
+
70
+
71
+ 🚨 Error Handling
72
+
73
+ ```
74
+ - Raises ValueError if required fields are missing.
75
+ - Raises TypeError if headers are not provided as a dictionary.
76
+
77
+ ```
78
+
79
+ ## πŸš€ Installation
80
+
81
+ This is a standalone utility. Copy the file into your Django project, or install it as a package using PyPI.
82
+
83
+ ---
84
+
85
+ ## 🧩 Requirements
86
+
87
+ - Python 3.8+
88
+ - Django >= 3.2
89
+
90
+ ---
91
+
92
+ ## πŸ§ͺ Basic Usage
93
+
94
+ Here's a simple example of how to send an email using `EmailSender`:
95
+
96
+ ```python
97
+ from email_sender import EmailSender
98
+
99
+ EmailSender.create()\
100
+ .from_address("no-reply@example.com")\
101
+ .to(["recipient@example.com"])\
102
+ .with_subject("Welcome!")\
103
+ .with_context({"username": "John"})\
104
+ .with_text_template("welcome.txt", folder_name="emails")\
105
+ .with_html_template("welcome.html", folder_name="emails")\
106
+ .send()
107
+ ```
108
+
109
+ ### Explanation:
110
+
111
+ - `.from_address("no-reply@example.com")`: Specifies the sender's email address.
112
+ - `.to(["recipient@example.com"])`: Specifies the recipient's email address.
113
+ - `.with_subject("Welcome!")`: The subject of the email.
114
+ - `.with_context({"username": "John"})`: Context for the email templates, allowing dynamic insertion of values (e.g., the recipient's name).
115
+ - `.with_text_template("welcome.txt", folder_name="emails")`: The path to the text-based email template. Here, we specify the folder name (`emails`) where the template is stored. If no folder name is provided, it defaults to `email_templates/`.
116
+ - `.with_html_template("welcome.html", folder_name="emails")`: The path to the HTML-based email template. Similarly, you can specify the folder name (`emails`) for this template.
117
+ - `.send()`: Sends the email.
118
+
119
+ ---
120
+
121
+ ## 🧱 Subclassing
122
+
123
+ You can also subclass the `EmailSender` class to create more specific types of emails.
124
+
125
+ ### Example: Password Reset Email
126
+
127
+ ```python
128
+ class PasswordResetEmail(EmailSender):
129
+ def __init__(self, user):
130
+ super().__init__()
131
+ self.user = user
132
+
133
+ def build(self):
134
+ return self\
135
+ .from_address("no-reply@example.com")\
136
+ .to([self.user.email])\
137
+ .with_subject("Reset Your Password")\
138
+ .with_context({"username": self.user.username, "reset_link": generate_reset_link(self.user)})\
139
+ .with_text_template("reset_password.txt", folder_name="emails")\
140
+ .with_html_template("reset_password.html", folder_name="emails")
141
+ ```
142
+
143
+ ### Usage:
144
+
145
+ ```python
146
+ PasswordResetEmail(user).build().send()
147
+ ```
148
+
149
+ Here, the `PasswordResetEmail` class uses `reset_password.txt` and `reset_password.html` templates from the `emails` folder.
150
+
151
+ ---
152
+
153
+ ## πŸ› οΈ Function-Based Abstractions
154
+
155
+ For a functional approach, you can also wrap `EmailSender` in specific functions to handle common email use cases.
156
+
157
+ ### Example: Sending a Verification Email
158
+
159
+ ```python
160
+
161
+ def send_verification_email(user):
162
+ html_verification_path = "verification/verification.html"
163
+ text_verification_path = "verification/verification.txt"
164
+ subject = "Verify Your Email"
165
+ from_email = "no-reply@example.com"
166
+
167
+ return EmailSender.create()\
168
+ .from_address(from_email)\
169
+ .to([user.email])\
170
+ .with_subject(subject)\
171
+ .with_context({
172
+ "username": user.username,
173
+ "verification_link": generate_verification_link(user)
174
+ })\
175
+ .with_text_template(text_verification_path, folder_name="emails")\
176
+ .with_html_template(html_verification_path, folder_name="emails")\
177
+ .send()
178
+ ```
179
+
180
+ ### Example: Sending a Registration Email
181
+
182
+ ```python
183
+ def send_registration_email(user):
184
+ html_registration_path = "registration/registration.html"
185
+ text_registration_path = "registration/registration.txt"
186
+
187
+ subject = "Welcome to the Platform!"
188
+ from_email = "no-reply@example.com"
189
+
190
+ return EmailSender.create()\
191
+ .from_address(from_email)\
192
+ .to([user.email])\
193
+ .with_subject(subject)\
194
+ .with_context({"username": user.username})\
195
+ .with_text_template(text_registration_path, folder_name="emails")\
196
+ .with_html_template(html_registration_path, folder_name="emails")\
197
+ .send()
198
+ ```
199
+
200
+ ### Advantages of this Approach:
201
+
202
+ - **Keeps your logic functional and simple**: It's straightforward to use and easy to test.
203
+ - **Keeps your email templates modular and easy to override**: Templates are organized in subfolders (e.g., `registration`, `verification`), making them easier to manage.
204
+ - **Clean and maintainable codebase**: You don’t have to subclass `EmailSender` each time, reducing complexity.
205
+
206
+ ---
207
+
208
+
209
+ ---
210
+
211
+ ## πŸ“ Templates
212
+
213
+ Templates must reside inside a dedicated `email_templates/` directory, which should exist inside your Django template directory.
214
+
215
+ This folder can contain your own structure to help organise different types of emails. For example:
216
+
217
+ ```
218
+ Example
219
+
220
+ project/
221
+ β”œβ”€β”€ templates/
222
+ β”‚ └── email_templates/
223
+ β”‚ └── registration/
224
+ β”‚ β”œβ”€β”€ registration.html
225
+ β”‚ └── registration.txt
226
+ ```
227
+
228
+ When calling `with_html_template()` or `with_text_template()`, you can provide the subfolder and filename like so:
229
+
230
+ ```python
231
+ EmailSender.create()
232
+ .with_html_template("registration.html", folder_name="registration")
233
+ .with_text_template("registration.txt", folder_name="registration")
234
+ ```
235
+
236
+ You **must** have both an `.html` and `.txt` version of the email template. These are required for rich content and email client compatibility.
237
+
238
+ ---
239
+
240
+
241
+ ## πŸ’‘ Tips
242
+
243
+ - You can subclass `EmailSender` for different email types or simply wrap it in functions.
244
+ - Organise your templates by email type (`registration/`, `verification/`, etc.)
245
+ - Subject and context are fully customisable.
246
+
247
+ ---
248
+
@@ -0,0 +1,219 @@
1
+ # πŸ“§ Django Email Sender Utility
2
+
3
+ A clean, reusable, lightweight and chainable utility class for sending emails in Django using templates. It supports both HTML and plain text templates, context injection, and flexible usage β€” either directly, via subclassing, or abstracted into functions.
4
+
5
+
6
+ 🧠 Why Use This?
7
+
8
+ While Django already provides a way to send emails, it can become verbose and repetitive. EmailSender abstracts the boilerplate and lets you send templated emails fluently.
9
+
10
+
11
+ ## ✨ Features
12
+
13
+ - Chainable API (`.to()`, `.from_address()`, etc.)
14
+ - Supports HTML and plain text templates
15
+ - Uses Django's template system for dynamic content
16
+ - Easy to integrate and override
17
+ - Encourages clean code and reusability
18
+ - Supports subclassing or functional abstractions
19
+
20
+ ---
21
+
22
+
23
+ ---
24
+
25
+ πŸ›  Available Methods
26
+
27
+ Method Description
28
+ ```
29
+
30
+ - create() # Class factory method to instantiate the EmailSender.
31
+ - from_address(email) # Sets the sender’s email address.
32
+ - to(recipients) # Accepts a string or list of recipient email addresses.
33
+ - with_subject(subject) # Sets the email subject.
34
+ - with_context(context) # Context dictionary used in the templates.
35
+ - with_text_template(path) # Path to the plain text email template.
36
+ - with_html_template(path) # Path to the HTML email template.
37
+ - with_headers(headers) # Optional custom headers as a dictionary.
38
+ - send() # Sends the email. Returns the number of successfully delivered messages.
39
+ ```
40
+
41
+
42
+ 🚨 Error Handling
43
+
44
+ ```
45
+ - Raises ValueError if required fields are missing.
46
+ - Raises TypeError if headers are not provided as a dictionary.
47
+
48
+ ```
49
+
50
+ ## πŸš€ Installation
51
+
52
+ This is a standalone utility. Copy the file into your Django project, or install it as a package using PyPI.
53
+
54
+ ---
55
+
56
+ ## 🧩 Requirements
57
+
58
+ - Python 3.8+
59
+ - Django >= 3.2
60
+
61
+ ---
62
+
63
+ ## πŸ§ͺ Basic Usage
64
+
65
+ Here's a simple example of how to send an email using `EmailSender`:
66
+
67
+ ```python
68
+ from email_sender import EmailSender
69
+
70
+ EmailSender.create()\
71
+ .from_address("no-reply@example.com")\
72
+ .to(["recipient@example.com"])\
73
+ .with_subject("Welcome!")\
74
+ .with_context({"username": "John"})\
75
+ .with_text_template("welcome.txt", folder_name="emails")\
76
+ .with_html_template("welcome.html", folder_name="emails")\
77
+ .send()
78
+ ```
79
+
80
+ ### Explanation:
81
+
82
+ - `.from_address("no-reply@example.com")`: Specifies the sender's email address.
83
+ - `.to(["recipient@example.com"])`: Specifies the recipient's email address.
84
+ - `.with_subject("Welcome!")`: The subject of the email.
85
+ - `.with_context({"username": "John"})`: Context for the email templates, allowing dynamic insertion of values (e.g., the recipient's name).
86
+ - `.with_text_template("welcome.txt", folder_name="emails")`: The path to the text-based email template. Here, we specify the folder name (`emails`) where the template is stored. If no folder name is provided, it defaults to `email_templates/`.
87
+ - `.with_html_template("welcome.html", folder_name="emails")`: The path to the HTML-based email template. Similarly, you can specify the folder name (`emails`) for this template.
88
+ - `.send()`: Sends the email.
89
+
90
+ ---
91
+
92
+ ## 🧱 Subclassing
93
+
94
+ You can also subclass the `EmailSender` class to create more specific types of emails.
95
+
96
+ ### Example: Password Reset Email
97
+
98
+ ```python
99
+ class PasswordResetEmail(EmailSender):
100
+ def __init__(self, user):
101
+ super().__init__()
102
+ self.user = user
103
+
104
+ def build(self):
105
+ return self\
106
+ .from_address("no-reply@example.com")\
107
+ .to([self.user.email])\
108
+ .with_subject("Reset Your Password")\
109
+ .with_context({"username": self.user.username, "reset_link": generate_reset_link(self.user)})\
110
+ .with_text_template("reset_password.txt", folder_name="emails")\
111
+ .with_html_template("reset_password.html", folder_name="emails")
112
+ ```
113
+
114
+ ### Usage:
115
+
116
+ ```python
117
+ PasswordResetEmail(user).build().send()
118
+ ```
119
+
120
+ Here, the `PasswordResetEmail` class uses `reset_password.txt` and `reset_password.html` templates from the `emails` folder.
121
+
122
+ ---
123
+
124
+ ## πŸ› οΈ Function-Based Abstractions
125
+
126
+ For a functional approach, you can also wrap `EmailSender` in specific functions to handle common email use cases.
127
+
128
+ ### Example: Sending a Verification Email
129
+
130
+ ```python
131
+
132
+ def send_verification_email(user):
133
+ html_verification_path = "verification/verification.html"
134
+ text_verification_path = "verification/verification.txt"
135
+ subject = "Verify Your Email"
136
+ from_email = "no-reply@example.com"
137
+
138
+ return EmailSender.create()\
139
+ .from_address(from_email)\
140
+ .to([user.email])\
141
+ .with_subject(subject)\
142
+ .with_context({
143
+ "username": user.username,
144
+ "verification_link": generate_verification_link(user)
145
+ })\
146
+ .with_text_template(text_verification_path, folder_name="emails")\
147
+ .with_html_template(html_verification_path, folder_name="emails")\
148
+ .send()
149
+ ```
150
+
151
+ ### Example: Sending a Registration Email
152
+
153
+ ```python
154
+ def send_registration_email(user):
155
+ html_registration_path = "registration/registration.html"
156
+ text_registration_path = "registration/registration.txt"
157
+
158
+ subject = "Welcome to the Platform!"
159
+ from_email = "no-reply@example.com"
160
+
161
+ return EmailSender.create()\
162
+ .from_address(from_email)\
163
+ .to([user.email])\
164
+ .with_subject(subject)\
165
+ .with_context({"username": user.username})\
166
+ .with_text_template(text_registration_path, folder_name="emails")\
167
+ .with_html_template(html_registration_path, folder_name="emails")\
168
+ .send()
169
+ ```
170
+
171
+ ### Advantages of this Approach:
172
+
173
+ - **Keeps your logic functional and simple**: It's straightforward to use and easy to test.
174
+ - **Keeps your email templates modular and easy to override**: Templates are organized in subfolders (e.g., `registration`, `verification`), making them easier to manage.
175
+ - **Clean and maintainable codebase**: You don’t have to subclass `EmailSender` each time, reducing complexity.
176
+
177
+ ---
178
+
179
+
180
+ ---
181
+
182
+ ## πŸ“ Templates
183
+
184
+ Templates must reside inside a dedicated `email_templates/` directory, which should exist inside your Django template directory.
185
+
186
+ This folder can contain your own structure to help organise different types of emails. For example:
187
+
188
+ ```
189
+ Example
190
+
191
+ project/
192
+ β”œβ”€β”€ templates/
193
+ β”‚ └── email_templates/
194
+ β”‚ └── registration/
195
+ β”‚ β”œβ”€β”€ registration.html
196
+ β”‚ └── registration.txt
197
+ ```
198
+
199
+ When calling `with_html_template()` or `with_text_template()`, you can provide the subfolder and filename like so:
200
+
201
+ ```python
202
+ EmailSender.create()
203
+ .with_html_template("registration.html", folder_name="registration")
204
+ .with_text_template("registration.txt", folder_name="registration")
205
+ ```
206
+
207
+ You **must** have both an `.html` and `.txt` version of the email template. These are required for rich content and email client compatibility.
208
+
209
+ ---
210
+
211
+
212
+ ## πŸ’‘ Tips
213
+
214
+ - You can subclass `EmailSender` for different email types or simply wrap it in functions.
215
+ - Organise your templates by email type (`registration/`, `verification/`, etc.)
216
+ - Subject and context are fully customisable.
217
+
218
+ ---
219
+
@@ -0,0 +1,301 @@
1
+ from typing import Optional, List, Dict, Union
2
+ from django.core.mail import EmailMultiAlternatives
3
+ from django.template.loader import render_to_string
4
+ from pathlib import Path
5
+ from os.path import join
6
+
7
+ from os import getenv
8
+
9
+ from django_email_sender.exceptions import EmailTemplateDirNotFound, EmailTemplateNotFound, TemplateDirNotFound, EmailSenderError
10
+
11
+
12
+ # Build paths inside the project like this: BASE_DIR / 'subdir'.
13
+ BASE_DIR = Path(__file__).resolve().parent.parent
14
+
15
+
16
+ TEMPLATES_DIR = join(BASE_DIR, "templates")
17
+ EMAIL_TEMPLATES_DIR = join(TEMPLATES_DIR, "emails_templates")
18
+
19
+ from os.path import exists
20
+
21
+
22
+ class EmailSender:
23
+ """
24
+ An email sender that allows you to send emails by
25
+ using a chaining method. This is mostly to be used in
26
+ the Django eco-system.
27
+
28
+ To use it in a Django eco-system, the following settings must be configured:
29
+
30
+ settings.py:
31
+ EMAIL_USE_TLS = True
32
+ EMAIL_HOST = 'smtp.gmail.com'
33
+ EMAIL_PORT = 587
34
+ EMAIL_HOST_USER = 'some@emailhere.com'
35
+ EMAIL_HOST_PASSWORD = 'password'
36
+
37
+ templates:
38
+ - Specify paths to your email templates.
39
+
40
+ Example usages:
41
+ You can easily abstract the `EmailSender` class into specific methods for sending different types of emails.
42
+
43
+ For example, to send a verification email:
44
+
45
+ def send_verification_email(user):
46
+
47
+ subject = "Verify Your Email"
48
+ from_email = "no-reply@example.com"
49
+
50
+ return EmailSender.create()\
51
+ .from_address(from_email)\
52
+ .to([user.email])\
53
+ .with_subject(subject)\
54
+ .with_context({
55
+ "username": user.username,
56
+ "verification_link": generate_verification_link(user)
57
+ })\
58
+ .with_text_template(folder_name="email", template_name="verification.txt")\
59
+ .with_html_template(folder_name="email", template_name="verification.html")\
60
+ .send()
61
+
62
+ Similarly, you can create other email methods such as:
63
+
64
+ def send_registration_email(user):
65
+
66
+ subject = "Welcome to the Platform!"
67
+ from_email = "no-reply@example.com"
68
+
69
+ return EmailSender.create()\
70
+ .from_address(from_email)\
71
+ .to([user.email])\
72
+ .with_subject(subject)\
73
+ .with_context({"username": user.username})\
74
+ .with_text_template(folder_name='emails', template_name='registration.html')\
75
+ .with_html_template(folder_name='emails', template_name='registeration.txt')\
76
+ .send()
77
+
78
+ This approach allows you to abstract email sending into different functions for
79
+ various use cases (e.g., registration, verification, password reset),
80
+ but at the same time allows the email sender class to be a single source of truth while allowing the
81
+ sending logic clean, consistent, and reusable.
82
+ """
83
+
84
+ def __init__(self):
85
+ """
86
+ Initialize an empty email configuration.
87
+ """
88
+ self.from_email: Optional[str] = None
89
+ self.to_email: List[str] = []
90
+ self.subject: Optional[str] = None
91
+ self.html_template: Optional[str] = None
92
+ self.text_template: Optional[str] = None
93
+ self.context: Dict = {}
94
+ self.headers: Dict = {}
95
+
96
+ @classmethod
97
+ def create(cls) -> "EmailSender":
98
+ """
99
+ Class factory method to initialize the EmailSender for method chaining.
100
+
101
+ Returns:
102
+ EmailSender: A new instance of EmailSender.
103
+ """
104
+ return cls()
105
+
106
+ def from_address(self, email: str) -> "EmailSender":
107
+ """
108
+ Set the sender's email address.
109
+
110
+ Args:
111
+ email (str): The sender's email address.
112
+
113
+ Returns:
114
+ EmailSender: The current instance for chaining.
115
+ """
116
+ self.from_email = email
117
+ return self
118
+
119
+ def to(self, recipients: Union[str, List[str]]) -> "EmailSender":
120
+ """
121
+ Set the recipient(s) of the email.
122
+
123
+ If a single email is provided as a string, it converts it into a list.
124
+ Otherwise, it assumes a list of email addresses.
125
+
126
+ Args:
127
+ recipients (Union[str, List[str]]): A single email address or a list of email addresses.
128
+
129
+
130
+ Returns:
131
+ EmailSender: The current instance for chaining.
132
+ """
133
+ self.to_email = [recipients] if isinstance(recipients, str) else recipients
134
+ return self
135
+
136
+ def with_subject(self, subject: str) -> "EmailSender":
137
+ """
138
+ Set the subject of the email.
139
+
140
+ Args:
141
+ subject (str): The subject line.
142
+
143
+ Returns:
144
+ EmailSender: The current instance for chaining.
145
+ """
146
+ self.subject = subject
147
+ return self
148
+
149
+ def with_context(self, context: Dict) -> "EmailSender":
150
+ """
151
+ Set the context dictionary for rendering templates.
152
+
153
+ Args:
154
+ context (Dict): Context variables for use in templates.
155
+
156
+ Returns:
157
+ EmailSender: The current instance for chaining.
158
+ """
159
+ self.context = context
160
+ return self
161
+
162
+ def with_html_template(self, template_name: str, folder_name: str = None) -> "EmailSender":
163
+ """
164
+ Set the HTML template path.
165
+
166
+ Args:
167
+ template_name (str): The name of the plain text template file (e.g. 'welcome.html').
168
+ folder_name (str, optional): The name of the subfolder inside the 'email_templates' directory
169
+ where this template is located. If None, template is assumed to
170
+ reside directly in the 'email_templates' folder.
171
+
172
+ Returns:
173
+ EmailSender: The current instance for chaining.
174
+ """
175
+ self.html_template = self._create_path(folder_name, template_name)
176
+ return self
177
+
178
+ def with_text_template(self, template_name: str, folder_name: str = None) -> "EmailSender":
179
+ """
180
+ Set the plain text template path for the email.
181
+
182
+ Args:
183
+ template_name (str): The name of the plain text template file (e.g. 'welcome.txt').
184
+ folder_name (str, optional): The name of the subfolder inside the 'email_templates' directory
185
+ where this template is located. If None, template is assumed to
186
+ reside directly in the 'email_templates' folder.
187
+
188
+ Returns:
189
+ EmailSender: The current instance for method chaining.
190
+ """
191
+ self.text_template = self._create_path(template_name, folder_name)
192
+ return self
193
+
194
+ def _does_template_path_exists(email_path):
195
+ """
196
+ Checks whether a given template path exists within the email templates directory.
197
+
198
+ This method performs the following checks:
199
+ - Ensures the base template directory exists (e.g., TEMPLATES_DIR).
200
+ - Ensures the 'email_templates' folder exists within the base directory.
201
+ - Confirms the specific template file exists at the given path.
202
+
203
+ Args:
204
+ email_path (str): The full relative path to the template file to check.
205
+
206
+ Raises:
207
+ TemplateDirNotFound: If the base template directory is missing.
208
+ EmailTemplateDirNotFound: If the 'email_templates' folder is missing.
209
+ TemplateNotFound: If the given email template file does not exist.
210
+ """
211
+
212
+ if not exists(TEMPLATES_DIR):
213
+ raise TemplateDirNotFound("The template directory wasn't found")
214
+ if not exists(EMAIL_TEMPLATES_DIR):
215
+ raise EmailTemplateDirNotFound("The emails_templates folder wasn't found")
216
+ if not exists(email_path):
217
+ raise EmailTemplateNotFound("The email template wasn't found")
218
+
219
+ def _create_path(self, template_name: str, folder_name: str = None):
220
+ """
221
+ Constructs the full path to an email template file inside the 'email_templates' directory.
222
+
223
+ Allows for an optional subfolder to help organise templates by category (e.g., registration, password_reset).
224
+
225
+ Args:
226
+ template_name (str): Name of the template file (e.g., 'welcome.html').
227
+ folder_name (str, optional): Name of the subfolder inside 'email_templates'. Defaults to None.
228
+
229
+ Returns:
230
+ str: The combined relative path to the desired template.
231
+
232
+ Raises:
233
+ ValueError: If template_name is not a string, or folder_name is not a string or None.
234
+ """
235
+
236
+ if folder_name != None and not isinstance(folder_name, str):
237
+ raise ValueError("The folder name must be a string or None")
238
+ if not isinstance(template_name, str):
239
+ raise ValueError("The template name must be a string or None")
240
+
241
+ if folder_name is None:
242
+ return join(EMAIL_TEMPLATES_DIR, template_name)
243
+ return join(EMAIL_TEMPLATES_DIR, folder_name, template_name)
244
+
245
+
246
+ def with_headers(self, headers: Optional[Dict] = None) -> "EmailSender":
247
+ """
248
+ Set custom headers for the email.
249
+
250
+ Args:
251
+ headers (Optional[Dict], optional): A dictionary of headers to include in the email. Default is an empty dictionary.
252
+
253
+ Raises:
254
+ TypeError: If headers is not a dictionary.
255
+
256
+ Returns:
257
+ EmailSender: The current instance for chaining.
258
+ """
259
+ if headers is None:
260
+ headers = {}
261
+ if not isinstance(headers, dict):
262
+ raise TypeError(f"Headers must be a dictionary, got {type(headers)} instead.")
263
+ self.headers = headers
264
+ return self
265
+
266
+ def send(self) -> int:
267
+ """
268
+ Send the email using Django's email backend.
269
+
270
+ This will render the text and HTML templates, attach the HTML alternative, and send the email.
271
+
272
+ Raises:
273
+ ValueError: If any required fields are missing before sending.
274
+ EmailSendError: Raises an EmailSendError if an error occurs while sending an email.
275
+
276
+ Returns:
277
+ int: The number of successfully delivered messages (typically 1 if successful).
278
+ """
279
+ if not all([self.from_email, self.to_email, self.subject, self.html_template, self.text_template]):
280
+ raise ValueError("All email components (from, to, subject, html, text) must be set before sending.")
281
+
282
+ self._does_template_path_exists(self.text_template)
283
+ self._does_template_path_exists(self.html_template)
284
+
285
+ text_content = render_to_string(self.text_template, context=self.context)
286
+ html_content = render_to_string(self.html_template, context=self.context)
287
+
288
+ msg = EmailMultiAlternatives(
289
+ subject=self.subject,
290
+ body=text_content,
291
+ from_email=self.from_email,
292
+ to=self.to_email,
293
+ headers=self.headers or None
294
+ )
295
+
296
+ msg.attach_alternative(html_content, "text/html")
297
+
298
+ try:
299
+ return msg.send()
300
+ except EmailSenderError as e:
301
+ raise EmailSenderError(str(e))
@@ -0,0 +1,18 @@
1
+ class EmailSenderError(Exception):
2
+ """Base exception for all email sender errors."""
3
+ pass
4
+
5
+
6
+ class EmailTemplateDirNotFound(EmailSenderError):
7
+ """Raised when the email_templates directory is not found."""
8
+ pass
9
+
10
+
11
+ class EmailTemplateNotFound(EmailSenderError):
12
+ """Raised when a specific email template file is missing."""
13
+ pass
14
+
15
+
16
+ class TemplateDirNotFound(EmailSenderError):
17
+ """Raised when a template directory does not exist."""
18
+ pass
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-email-sender
3
+ Version: 1.0.0
4
+ Summary: A chainable Django email sender utility.
5
+ Home-page: https://github.com/EgbieAndersonUku1/django-email-sender
6
+ Author: Egbie Uku
7
+ Author-email: egbieuku@hotmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Framework :: Django
10
+ Classifier: Framework :: Django :: 3.2
11
+ Classifier: Framework :: Django :: 4.0
12
+ Classifier: Framework :: Django :: 5.0
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.7
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENCE
18
+ Requires-Dist: Django<6.0,>=3.2
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # πŸ“§ Django Email Sender Utility
31
+
32
+ A clean, reusable, lightweight and chainable utility class for sending emails in Django using templates. It supports both HTML and plain text templates, context injection, and flexible usage β€” either directly, via subclassing, or abstracted into functions.
33
+
34
+
35
+ 🧠 Why Use This?
36
+
37
+ While Django already provides a way to send emails, it can become verbose and repetitive. EmailSender abstracts the boilerplate and lets you send templated emails fluently.
38
+
39
+
40
+ ## ✨ Features
41
+
42
+ - Chainable API (`.to()`, `.from_address()`, etc.)
43
+ - Supports HTML and plain text templates
44
+ - Uses Django's template system for dynamic content
45
+ - Easy to integrate and override
46
+ - Encourages clean code and reusability
47
+ - Supports subclassing or functional abstractions
48
+
49
+ ---
50
+
51
+
52
+ ---
53
+
54
+ πŸ›  Available Methods
55
+
56
+ Method Description
57
+ ```
58
+
59
+ - create() # Class factory method to instantiate the EmailSender.
60
+ - from_address(email) # Sets the sender’s email address.
61
+ - to(recipients) # Accepts a string or list of recipient email addresses.
62
+ - with_subject(subject) # Sets the email subject.
63
+ - with_context(context) # Context dictionary used in the templates.
64
+ - with_text_template(path) # Path to the plain text email template.
65
+ - with_html_template(path) # Path to the HTML email template.
66
+ - with_headers(headers) # Optional custom headers as a dictionary.
67
+ - send() # Sends the email. Returns the number of successfully delivered messages.
68
+ ```
69
+
70
+
71
+ 🚨 Error Handling
72
+
73
+ ```
74
+ - Raises ValueError if required fields are missing.
75
+ - Raises TypeError if headers are not provided as a dictionary.
76
+
77
+ ```
78
+
79
+ ## πŸš€ Installation
80
+
81
+ This is a standalone utility. Copy the file into your Django project, or install it as a package using PyPI.
82
+
83
+ ---
84
+
85
+ ## 🧩 Requirements
86
+
87
+ - Python 3.8+
88
+ - Django >= 3.2
89
+
90
+ ---
91
+
92
+ ## πŸ§ͺ Basic Usage
93
+
94
+ Here's a simple example of how to send an email using `EmailSender`:
95
+
96
+ ```python
97
+ from email_sender import EmailSender
98
+
99
+ EmailSender.create()\
100
+ .from_address("no-reply@example.com")\
101
+ .to(["recipient@example.com"])\
102
+ .with_subject("Welcome!")\
103
+ .with_context({"username": "John"})\
104
+ .with_text_template("welcome.txt", folder_name="emails")\
105
+ .with_html_template("welcome.html", folder_name="emails")\
106
+ .send()
107
+ ```
108
+
109
+ ### Explanation:
110
+
111
+ - `.from_address("no-reply@example.com")`: Specifies the sender's email address.
112
+ - `.to(["recipient@example.com"])`: Specifies the recipient's email address.
113
+ - `.with_subject("Welcome!")`: The subject of the email.
114
+ - `.with_context({"username": "John"})`: Context for the email templates, allowing dynamic insertion of values (e.g., the recipient's name).
115
+ - `.with_text_template("welcome.txt", folder_name="emails")`: The path to the text-based email template. Here, we specify the folder name (`emails`) where the template is stored. If no folder name is provided, it defaults to `email_templates/`.
116
+ - `.with_html_template("welcome.html", folder_name="emails")`: The path to the HTML-based email template. Similarly, you can specify the folder name (`emails`) for this template.
117
+ - `.send()`: Sends the email.
118
+
119
+ ---
120
+
121
+ ## 🧱 Subclassing
122
+
123
+ You can also subclass the `EmailSender` class to create more specific types of emails.
124
+
125
+ ### Example: Password Reset Email
126
+
127
+ ```python
128
+ class PasswordResetEmail(EmailSender):
129
+ def __init__(self, user):
130
+ super().__init__()
131
+ self.user = user
132
+
133
+ def build(self):
134
+ return self\
135
+ .from_address("no-reply@example.com")\
136
+ .to([self.user.email])\
137
+ .with_subject("Reset Your Password")\
138
+ .with_context({"username": self.user.username, "reset_link": generate_reset_link(self.user)})\
139
+ .with_text_template("reset_password.txt", folder_name="emails")\
140
+ .with_html_template("reset_password.html", folder_name="emails")
141
+ ```
142
+
143
+ ### Usage:
144
+
145
+ ```python
146
+ PasswordResetEmail(user).build().send()
147
+ ```
148
+
149
+ Here, the `PasswordResetEmail` class uses `reset_password.txt` and `reset_password.html` templates from the `emails` folder.
150
+
151
+ ---
152
+
153
+ ## πŸ› οΈ Function-Based Abstractions
154
+
155
+ For a functional approach, you can also wrap `EmailSender` in specific functions to handle common email use cases.
156
+
157
+ ### Example: Sending a Verification Email
158
+
159
+ ```python
160
+
161
+ def send_verification_email(user):
162
+ html_verification_path = "verification/verification.html"
163
+ text_verification_path = "verification/verification.txt"
164
+ subject = "Verify Your Email"
165
+ from_email = "no-reply@example.com"
166
+
167
+ return EmailSender.create()\
168
+ .from_address(from_email)\
169
+ .to([user.email])\
170
+ .with_subject(subject)\
171
+ .with_context({
172
+ "username": user.username,
173
+ "verification_link": generate_verification_link(user)
174
+ })\
175
+ .with_text_template(text_verification_path, folder_name="emails")\
176
+ .with_html_template(html_verification_path, folder_name="emails")\
177
+ .send()
178
+ ```
179
+
180
+ ### Example: Sending a Registration Email
181
+
182
+ ```python
183
+ def send_registration_email(user):
184
+ html_registration_path = "registration/registration.html"
185
+ text_registration_path = "registration/registration.txt"
186
+
187
+ subject = "Welcome to the Platform!"
188
+ from_email = "no-reply@example.com"
189
+
190
+ return EmailSender.create()\
191
+ .from_address(from_email)\
192
+ .to([user.email])\
193
+ .with_subject(subject)\
194
+ .with_context({"username": user.username})\
195
+ .with_text_template(text_registration_path, folder_name="emails")\
196
+ .with_html_template(html_registration_path, folder_name="emails")\
197
+ .send()
198
+ ```
199
+
200
+ ### Advantages of this Approach:
201
+
202
+ - **Keeps your logic functional and simple**: It's straightforward to use and easy to test.
203
+ - **Keeps your email templates modular and easy to override**: Templates are organized in subfolders (e.g., `registration`, `verification`), making them easier to manage.
204
+ - **Clean and maintainable codebase**: You don’t have to subclass `EmailSender` each time, reducing complexity.
205
+
206
+ ---
207
+
208
+
209
+ ---
210
+
211
+ ## πŸ“ Templates
212
+
213
+ Templates must reside inside a dedicated `email_templates/` directory, which should exist inside your Django template directory.
214
+
215
+ This folder can contain your own structure to help organise different types of emails. For example:
216
+
217
+ ```
218
+ Example
219
+
220
+ project/
221
+ β”œβ”€β”€ templates/
222
+ β”‚ └── email_templates/
223
+ β”‚ └── registration/
224
+ β”‚ β”œβ”€β”€ registration.html
225
+ β”‚ └── registration.txt
226
+ ```
227
+
228
+ When calling `with_html_template()` or `with_text_template()`, you can provide the subfolder and filename like so:
229
+
230
+ ```python
231
+ EmailSender.create()
232
+ .with_html_template("registration.html", folder_name="registration")
233
+ .with_text_template("registration.txt", folder_name="registration")
234
+ ```
235
+
236
+ You **must** have both an `.html` and `.txt` version of the email template. These are required for rich content and email client compatibility.
237
+
238
+ ---
239
+
240
+
241
+ ## πŸ’‘ Tips
242
+
243
+ - You can subclass `EmailSender` for different email types or simply wrap it in functions.
244
+ - Organise your templates by email type (`registration/`, `verification/`, etc.)
245
+ - Subject and context are fully customisable.
246
+
247
+ ---
248
+
@@ -0,0 +1,13 @@
1
+ LICENCE
2
+ MANIFEST.in
3
+ README.md
4
+ setup.py
5
+ django_email_sender/__init__.py
6
+ django_email_sender/email_sender.py
7
+ django_email_sender/exceptions.py
8
+ django_email_sender.egg-info/PKG-INFO
9
+ django_email_sender.egg-info/SOURCES.txt
10
+ django_email_sender.egg-info/dependency_links.txt
11
+ django_email_sender.egg-info/requires.txt
12
+ django_email_sender.egg-info/top_level.txt
13
+ tests/test_email_sender.py
@@ -0,0 +1 @@
1
+ Django<6.0,>=3.2
@@ -0,0 +1 @@
1
+ django_email_sender
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="django-email-sender",
5
+ version="1.0.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "Django>=3.2,<6.0",
9
+ ],
10
+ author="Egbie Uku",
11
+ author_email="egbieuku@hotmail.com",
12
+ description="A chainable Django email sender utility.",
13
+ long_description=open("README.md", encoding="utf-8").read(),
14
+ long_description_content_type="text/markdown",
15
+ url="https://github.com/EgbieAndersonUku1/django-email-sender",
16
+ classifiers=[
17
+ "Programming Language :: Python :: 3",
18
+ "Framework :: Django",
19
+ "Framework :: Django :: 3.2",
20
+ "Framework :: Django :: 4.0",
21
+ "Framework :: Django :: 5.0",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ ],
25
+ python_requires='>=3.7',
26
+ )
@@ -0,0 +1,23 @@
1
+ from django.test import TestCase
2
+ from email_sender.email_sender import EmailSender
3
+
4
+
5
+ class TestEmailSender(TestCase):
6
+ def test_email_sender_creates_and_sends_email(self):
7
+
8
+ # Example test for email sending logic
9
+ email_sender = EmailSender.create()\
10
+ .from_address("no-reply@example.com")\
11
+ .to(["test@example.com"])\
12
+ .with_subject("Test Email")\
13
+ .with_context({"username": "testuser"})\
14
+ .with_text_template("emails/test.txt")\
15
+ .with_html_template("emails/test.html")
16
+
17
+ # Test if the sender was set correctly
18
+ self.assertEqual(email_sender.from_address, "no-reply@example.com")
19
+ self.assertEqual(email_sender.subject, "Test Email")
20
+
21
+ # Here, you can mock `send_mail` to ensure it gets called
22
+ with self.assertRaises("SomeMockException"):
23
+ email_sender.send()