Keeping DRY with sending mail in Django - django

I have the following two blocks of code in a LOT of my views. Im looking for a way to abstract them so that instead of repeating this code in every view. The receipent, subject line and body will vary of course, so I would like to be able to pass those strings to this function--"function" is the right term to use, correct?
mailt = loader.get_template('membership/signup_email.txt')
mailc = Context({
'signin_url': signin_url,
'name': firstname + ' ' + lastname,
'username': username,
'membership_level': membership_level.name,
'membership_number': membership_number,
'payment_plan': payment_plan
})
msg = EmailMessage(
'You are now a Member!',
mailt.render(mailc),
'membership#domain.org',
[email]
)
msg.content_subtype = "html"
msg.send()
# Nofity our staff
admin_mailt = loader.get_template('membership/signup_admin_email.txt')
admin_mailc = Context({
'site': current_site,
'user': user,
'payment_plan': payment_plan
})
admin_msg = EmailMessage(
'[myproject] New Membership Signup',
admin_mailt.render(admin_mailc),
'membership#domain.org',
['membership#domain.org']
)
admin_msg.content_subtype = "html"
admin_msg.send()

I'm not sure where your variables are all coming from...but....could you simply create a function in say a utils.py? Put the above code in there and call it with parameters when needed. So your views might have something like follows. A call to a function you created elsewhere.
custom_send_mail(recipient, subject, body)

You could use the built in django email methods and abstract them a little to get what you want, it won't buy you that much, but here you go.
See the documentation here: http://docs.djangoproject.com/en/1.3/topics/email/
Here is how you use the built in django email method.
from django.core.mail import send_mail
send_mail('Subject here', 'Here is the message.', 'from#example.com',
['to#example.com'], fail_silently=False)
So one of your examples above would turn into this.
# Nofity our staff
admin_mailt = loader.get_template('membership/signup_admin_email.txt')
admin_mailc = Context({
'site': current_site,
'user': user,
'payment_plan': payment_plan
})
send_mail('[myproject] New Membership Signup', admin_mailt.render(admin_mailc), 'membership#domain.org', ['membership#domain.org'])
You could wrap this a little so that you could just pass in the template name and the context and it would make it a little cleaner.
send_my_email(subject, to_address, template, context, from_address='membership#domain.org'):
admin_mailt = loader.get_template(template)
send_mail(subject, admin_mailt.render(context), from_address, to_address)

Related

Django send bulk emails

I am working on a service to send bulk emails in django.
I have this method which is working well with celery
#shared_task(bind=True)
def send_mails(self,saved_id):
text = BroadCast.objects.get(id=saved_id)
attendees = EventAttendee.objects.filter(event__id=text.id)
message = text.body
subject = text.subject
document = text.attachment
recipient_list=[]
for attend in attendees:
text_content = render_to_string(template_text, {'name': attend.client_name, 'message':message})
html_content = render_to_string(template_html, {'name': attend.client_name,'message':message})
mail.send(
[attend.client_email],
email_from,
subject=subject,
html_message=html_content,
attachments = {str(document):document}
)
my challenge is that if i have for examples 1000 attendees, I will have to open 1000 connections which I believe is a very bad.
How can I restructure it so that I only open one connection and be able to send 1000 emails..
From Django's docs
django.core.mail.send_mass_mail() is intended to handle mass emailing.
Since you are sending html, you would need an extra step, consider the following piece of code from this stackoverflow answer:
from django.core.mail import get_connection, EmailMultiAlternatives
def send_mass_html_mail(datatuple, fail_silently=False, user=None, password=None,
connection=None):
"""
Given a datatuple of (subject, text_content, html_content, from_email,
recipient_list), sends each message to each recipient list. Returns the
number of emails sent.
If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
If auth_user and auth_password are set, they're used to log in.
If auth_user is None, the EMAIL_HOST_USER setting is used.
If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
"""
connection = connection or get_connection(
username=user, password=password, fail_silently=fail_silently)
messages = []
for subject, text, html, from_email, recipient in datatuple:
message = EmailMultiAlternatives(subject, text, from_email, recipient)
message.attach_alternative(html, 'text/html')
messages.append(message)
return connection.send_messages(messages)
Then you probably want to use send_mass_mail
only one connection to the mail server would be opened
So construct a tuple of messages for all the emails you want to send. (The linked official documentation does a good job explaining usage)

How to send Email with html page in Django?

I'm new in Django ! I don't know how to send email in Django. I refer Django documentation but it didn't help me . I need to send email with html page to different users .In models.py i have two values Name and Email. When i click button ,the html page should be send to appropriate user's Email
There are a lot of different solutions how to send emails in django.
You can use even php, or any scripting language if you feel it's complicated to use only python/django code.
Just an example of email utility from custom email subscription:
email_utility.py:
import logging, traceback
from django.urls import reverse
import requests
from django.template.loader import get_template
from django.utils.html import strip_tags
from django.conf import settings
def send_email(data):
try:
url = "https://api.mailgun.net/v3/<domain-name>/messages"
status = requests.post(
url,
auth=("api", settings.MAILGUN_API_KEY),
data={"from": "YOUR NAME <admin#domain-name>",
"to": [data["email"]],
"subject": data["subject"],
"text": data["plain_text"],
"html": data["html_text"]}
)
logging.getLogger("info").info("Mail sent to " + data["email"] + ". status: " + str(status))
return status
except Exception as e:
logging.getLogger("error").error(traceback.format_exc())
return False
Don't forget to create a token which we will verify when user clicks the confirmation link. Token will be encrypted so that no one can tamper the data.
token = encrypt(email + constants.SEPARATOR + str(time.time()))
Also check this link and this.
Here is a naive exemple to leverage django send_mail:
import smtplib
from django.core.mail import send_mail
from django.utils.html import strip_tags
from django.template.loader import render_to_string
#user will be a queryset like:
users = User.objects.all() # or more specific query
subject = 'Subject'
from_email = 'from#xxx.com'
def send_email_to_users(users,subject,from_email):
full_traceback = []
for user in users:
to = [user.email] # list of people you want to sent mail to.
html_content = render_to_string('mail_template.html', {'title':'My Awesome email title', 'content' : 'Some email content', 'username':user.username}) # render with dynamic context you can retrieve in the html file
traceback = {}
try:
send_mail(subject,strip_tags(html_content),from_email, to, html_message=html_content, fail_silently=False)
traceback['status'] = True
except smtplib.SMTPException as e:
traceback['error'] = '%s (%s)' % (e.message, type(e))
traceback['status'] = False
full_traceback.append(traceback)
errors_to_return = []
error_not_found = []
for email in full_traceback:
if email['status']:
error_not_found.append(True)
else:
error_not_found.append(False)
errors_to_return.append(email['error'])
if False in error_not_found:
error_not_found = False
else:
error_not_found = True
return (error_not_found, errors_to_return)
#really naive view using the function on top
def my_email_view(request,user_id):
user = get_object_or_404(User, pk=user_id)
subject = 'Subject'
from_email = 'myemail#xxx.com'
email_sent, traceback = send_email_to_users(user, subject, from_email)
if email_sent:
return render(request,'sucess_template.html')
return render(request,'fail_template.html',{'email_errors' : traceback})
In your template mail_template.html:
<h1>{{title}}</h1>
<p>Dear {{username}},</p>
<p>{{content}}</p>
And don't forget to set the email settings in settings.py: https://docs.djangoproject.com/fr/2.2/ref/settings/#email-backend
Send_mail from docs :https://docs.djangoproject.com/fr/2.2/topics/email/#send-mail
Render_to_string from the doc: https://docs.djangoproject.com/fr/2.2/topics/templates/#django.template.loader.render_to_string

Sending EmailMultiAlternatives in mail_admins()

I have the below view code snippet and I would like to send email as EmailMultiAlternatives to the admins, the problem is...it doesn't render the html tags.
Kindly assist with some ideas.
subject = "A New Feedback"
ctx = {
'name': name,
'email': email,
'message': message
}
message = ('ecoke/includes/email_feedback.html', ctx)
mail_admins(subject, message, fail_silently=True, html_message='text/html')
mail_admins is a wrapper around send_mail.
In the send_mail documentation html_message is described as the HTML message to be attached to the email where as message is the plain text string.
From what you have above you will need to render (render_to_string) the message and attach it as HTML.
subject = "A New Feedback"
ctx = {
'name': name,
'email': email,
'message': message
}
html_message = render_to_string(
'ecoke/includes/email_feedback.html',
context=ctx
)
text_message = 'Hi %s, <plain text goes here>' % name
mail_admins(subject, text_message, fail_silently=True, html_message=html_message)
What happens is that you have two versions of the text; one for plain text one for HTML. As the HTML is not rendered by mail_admins you will need to render the template to HTML manually. The HTML message is attached as an alternative as illustrated here.

recipient_list send mail just to the first address in the list

I´m using signals to send mails to the users depending of some actions. In one of my signals I need to send the same mail to multiple users, my signal use post_save so for the parameter [recipient_list] I use this instance.email_list email_list is where I store the list of email addresses to send the mail, they are stored in this format user1#mail.com, user2#mail.com, user3#mail.com.
To send the emails I use EMAIL_HOST = 'smtp.gmail.com' the problem is that the only user who recive the email is the first one in the list. So I 'log in' in the gmail account to check the send emails section and actually in the "to" area of the email shows that was send to all the users in the list but the emails never arrive just for the first email address in the list.
I read that if google detect that the account sends a lot of messages could be blocked but only send like 5 emails at the same time and when I'm in the account never shows me some alert or something.
So the problem is how I send the emails or maybe some bad configuration of the gmail account?
Any help is really appreciated.
Sorry for my bad grammar.
EDIT: Here's my code.
forms.py
class MyForm(forms.Form):
userslist = forms.ModelChoiceField(queryset = User.objects.filter(here goes my condition to show the users), empty_label='List of users', label='Users', required=False)
emailaddress = forms.CharField(max_length=1000, label='Send to:', required=False)
comment = forms.CharField(widget=CKEditorUploadingWidget(), label="Comment:")
That form display a list of users to select the email address in the field emailaddress store the values. This is my Ajax to bring the email address:
views.py
class mails(TemplateView):
def get(self, request, *args, **kwargs):
id_user = request.GET['id']
us = User.objects.filter(id = id_user)
data = serializers.serialize('json', us, fields=('email'))
return HttpResponse(data, content_type='application/json')
And here's the <script> I use to populate the emailaddres field:
<script>
$('#id_userlist').on('change', concatenate);
function concatenate() {
var id = $(this).val();
$.ajax({
data: { 'id': id },
url: '/the_url_to_get_data/',
type: 'get',
success: function (data) {
var mail = ""
for (var i = 0; i < data.length; i++) {
mail += data[i].fields.email;
}
var orig = $('#id_emailaddress').val();
$('#id_emailaddress').val(orig + mail + ',');
}
})
}
</script>
The signal I use to send the mail is this:
#receiver(post_save, sender=ModelOfMyForm, dispatch_uid='mails_signal')
def mails_signal(sender, instance, **kwargs):
if kwargs.get('created', False):
if instance.emailaddress:
#Here goes the code for the subject, plane_message,
#from_email and template_message.
send_mail(subject, plane_message, from_email, [instance.emailaddress], fail_silently=False, html_message=template_message)
So if I select 4 users the info is save in this way in the database:
Then I 'log in' in the account to check the 'Sent Mail' section and check the detail of the mail and shows that was send to the 4 users but the only user who recibe the mail was the first in the list.
Your problem is that you are passing a comma-separated string of email addresses inside instance.emailaddress (first#gmail.com, second#hotmail.com, third#hotmail.com etc). Django expects a Python list of addresses, not a comma separated string. It will just ignore everything after the first comma.
Change your code as follows and it will work:
def mails_signal(sender, instance, **kwargs):
if kwargs.get('created', False):
if instance.emailaddress:
#Here goes the code for the subject, plane_message,
#from_email and template_message.
recipients = [r.strip() for r in instance.emailaddress.split(',')]
send_mail(subject, plane_message, from_email, recipients, fail_silently=False, html_message=template_message)

Pass a lazy translation string including variable to function in Django

Inside a Django view, I create a subject like that:
subject = _(u"%(user)s has posted a comment") % { 'user': user }
Then I pass this subject to a function, which handles email notifications:
send_notifications(request, subject, url)
In send_notifications, I iterate over all subscriptions and send emails. However, each user can have a different language, so I activate the user's language dynamically via Django's activate:
def send_notifications(request, subject, url):
from django.utils.translation import activate
for s in Subscription.objects.filter(url=url):
activate(s.user.userprofile.lang)
send_mail(subject, render_to_string('notification_email.txt', locals()), settings.SERVER_EMAIL, [s.user.email])
The template gets rendered in the correct language of each user. However, the subject is passed as an evaluated and translated string to send_notifications and thus, is not translated.
I played around with lazy translations and lambda functions as parameters, but without success. Any help appreciated :)
Instead of passing the translated subject, just pass it non translated:
subject = '%(user)s has posted a comment'
context = {'user': user}
def send_notifications(request, subject, url, context):
from django.utils.translation import activate
for s in Subscription.objects.filter(url=url):
activate(s.user.userprofile.lang)
send_mail(_(subject) % context, render_to_string('notification_email.txt', locals()), settings.SERVER_EMAIL, [s.user.email])
If you're not going to personalize the contents per user, then you might as well limit the number of renderings because that's a little confusing:
# also do your imports at the top to catch import issues early
from django.utils.translation import activate
from django.utils.translation import ugettext as _
def send_notifications(request, url,
translatable_subject, context,
body_template='notification_template.txt'):
previous_lang = None
for s in Subscription.objects.filter(url=url).order_by('user__userprofile__lang'):
if s.user.userprofile.lang != previous_lang:
activate(s.user.userprofile.lang)
subject = _(translatable_subject) % context
body = render_to_string(body_template, locals())
send_mail(subject, body, settings.SERVER_EMAIL, [s.user.email])
previous_lang = s.user.userprofile.lang
As such, it is much more obvious that you're not going to render emails per usage.
This slight rewrite should make you doubt about the original choice of a couple of names (locals, notification_template).
The above sample code is barely an "educated guess" and you should double check it and make sure you understand everything before you paste it.
Ok, found a solution myself. In case anybody runs into a similar problem:
from django.utils.translation import ugettext as _
# create subject as raw string in Django view
raw_subject = r"%(username)s has posted a comment"
# for the sake of generic variables, create a dictionary to pass to function
extra_context = { 'user': user }
# call function with raw string and dictionary as params
send_notifications(request, raw_subject, url, extra_context)
# translate the raw string inside send_notifications into the temporarily activated language
translated_subject = _(raw_subject) % extra_context
Appears to be working as desired :) Since we are working with several different notifications, I tried to avoid an extra template for each kind. However, calling a template with extra_context is also a possible solution.
I ended up creating a class that will handle the translation, passing the context of the string to this class.
from django.utils.translation import gettext_lazy as _
class Translatable(object):
def __init__(self, text, context):
self.text = text
self.context = context
def __str__(self):
return _(self.text).format(**self.context)
In your function, just call str(subject):
def send_notifications(request, subject, url):
from django.utils.translation import activate
for s in Subscription.objects.filter(url=url):
activate(s.user.userprofile.lang)
send_mail(str(subject), render_to_string('notification_email.txt', locals()), settings.SERVER_EMAIL, [s.user.email])
And passes subject to your function as following:
subject = Translatable(_("{user} has posted a comment"), context={'user': user})
send_notifications(request, subject, url)