passing django object context to sendgrid email via sendgrid-python API lib - python

my django app has a view where accounts can send out newsletter emails to its contacts and subscribers using Sendgrid's API. sending is working with a plaintext email:
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import (Mail, Subject, To, ReplyTo, SendAt, Content, From, CustomArg, Header)
def compose_email(request, send_to, *args, **kwargs):
...
if request.method == 'POST':
subject = request.POST.get('subject')
from_name = request.POST.get('from_name')
body = request.POST.get('body')
reply_to = request.POST.get('reply_to')
test_address = [request.POST.get('test_address')]
# send test email
if request.POST.get('do_test'):
if form.is_valid():
message = AccountEmailMessage(account=account, subject=subject,
from_name=from_name, destination=destination, body=body, reply_to=reply_to,
is_draft=True, is_sent=False)
message.save()
email = Mail(
subject=subject,
from_email=hi#app.foo,
html_content=body,
to_emails=test_address,
)
email.reply_to = ReplyTo(reply_to)
try:
sendgrid_client = SendGridAPIClient(settings.SENDGRID_API_KEY)
response = sendgrid_client.send(email)
message.sendgrid_id = response.headers['X-Message-Id']
message.save()
except Exception as e:
log.error(e)
messages.success(request, 'Test message has been successfully sent')
else:
messages.error(request, 'Please, check for errors')
this works. but we want to render django object properties (model fields via template tags) in an html email template from Account (account) [assume it's just a vanilla obj req query account = Account.objects.get(id=selected_account) in the view], and I'm not clear what's the recommended docs approach.
the attempt:
if request.method == 'POST':
subject = request.POST.get('subject')
from_name = request.POST.get('from_name')
body = request.POST.get('body')
reply_to = request.POST.get('reply_to')
if request.POST.get('send'):
if form.is_valid():
message = AccountEmailMessage(account=account, subject=subject,
from_name=from_name, destination=destination, body=body, reply_to=reply_to,
is_draft=False, is_sent=True)
message.save()
rendered = render_to_string('email/newsletter.html', {
'account': account,
'protocol': settings.DEFAULT_PROTOCOL,
'domain': settings.DOMAIN,
'message_body': body
})
email = Mail(
subject=subject,
from_email=hi#app.foo,
html_content=rendered,
to_emails=recipients,
mime_type='text/html'
)
email.reply_to = ReplyTo(reply_to)
try:
sendgrid_client = SendGridAPIClient(settings.SENDGRID_API_KEY)
response = sendgrid_client.send(email)
message.sendgrid_id = response.headers['X-Message-Id']
message.save()
except Exception as e:
log.error(e)
but on submit, this throws an err: NoReverseMatch: Reverse for 'account' not found. 'account' is not a valid view function or pattern name when I try to pass account as a kwarg to the context and render it as a string.
looking at the docs (https://github.com/sendgrid/sendgrid-python#use-cases) I see Mail() has a .dynamic_template_data property. that's very inefficient to process a large number of fields from the same obj, as well as attributes like image urls, and also requires use of legacy transactional templates (https://sendgrid.com/docs/ui/sending-email/create-and-edit-legacy-transactional-templates/). I see Sendgrid has a Personalization obj (https://sendgrid.com/docs/for-developers/sending-email/personalizations/) - is that the recommended way to implement this?

thanks to Iain on further testing realized we had two issues:
was attempting to encode a url in the template via {% url %} tag, that threw the NoReverseMatch
mime_type='text/html' isn't a valid kwarg for Mail(), removed that as well.
after (1) and (2) everything's working properly, no need for personalization

Related

How can I send a message to all posts in Django Allauth?

I am trying to send an email to all registered users in Django Allauth, but when I try to get a list of all users' emails and send it via the send_mail() method, I get an error :
too many values to unpack (expected 2)
When I manually specify the mail in the recipient_list list, then everything works. But I need it to be automatically sent to the emails of all users.
Tried to do through :
def email(request):
cleaned_data = super().clean()
title = cleaned_data.get('articles_title')
message = cleaned_data.get('articles_text')
recipient_list = User.objects.get('email')
email_from = 'mymail'
send_mail(title, message[:50], email_from, recipient_list)
return title, message
or iterate through the for loop:
def email(request):
cleaned_data = super().clean()
title = cleaned_data.get('articles_title')
message = cleaned_data.get('articles_text')
mails = User.objects.get('email')
recipient_list = []
for i in mails:
recipient_list.append(i)
email_from = 'mymail'
send_mail(title, message[:50], email_from, recipient_list)
return title, message
But nothing helps, does someone know some alternative method?

How to Link Sengrid to a Contact Form in Flask to Forward Email

I am writing a web application and am trying to use sendgrid to handle email delivery services.
I am writing the application in Flask.
Right now I have a contact form, and my problem is that the email only gets delivered if I send the e-mail from my pre-approved e-mail address with sendgrid. Obviously this is not good since everyone else who fills out the e-mail form will not have it go through.
Here's the code that I have:
ROUTE
app.config['MAIL_SERVER'] = 'smtp.sendgrid.net'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'apikey'
app.config['MAIL_PASSWORD'] = os.environ.get('SENDGRID_API_KEY')
app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER')
#app.route('/contact', methods=['GET', 'POST'])
def contactpage():
if request.method == 'POST':
print("Message sent")
print(request.form.to_dict())
m = message(request.form.to_dict())
m.send()
title = "Jonathan Bechtel contact form"
description = "Contact Jonathan Bechtel with questions about teaching or working with him"
return render_template("contact.html",
title=title,
description=description)
Here's my code for actually sending the e-mail:
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
class message():
def __init__(self, message_data):
for key in message_data:
print(key, message_data[key])
setattr(self, key, message_data[key])
def send(self):
message = Mail(
from_email = self.email,
to_emails = 'jonathanbechtel#gmail.com',
subject = 'Sample Email Message',
html_content = f'<strong>From: {self.email}</strong><br><strong>Reason: {self.reason}</strong><br><strong>Message:</strong>{self.message}')
try:
sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
response = sg.send(message)
except Exception as e:
print(e)
If I set the from address in my contact form to my own the e-mail works fine. However, if I use any other one the message does not go through and generates a 403 status code.
I think this means that I'm just not using the correct part of the API but am not sure where to start.
Thank you.
Twilio SendGrid developer evangelist here.
SendGrid does not allow you to send emails from just any email address. I can see that in this case you are just trying to create a contact form that only sends emails to your email address so being able to send from any email address might be useful. But consider a form that allowed users to set the to and the from address and you can see how that might get abused.
You can read more about sender identity and SendGrid here.
In the meantime, for your use-case here is what I would suggest.
Set the from email to your pre-approved email address, include the user's email address in the body of the email, as you are doing already. Then add the user's email as a reply-to email as well, that way you can respond to the email and it will be sent straight to the user.
I believe you can set the reply-to with the mail object's reply_to method:
def send(self):
message = Mail(
from_email = APPROVED_SENDGRID_EMAIL,
to_emails = 'jonathanbechtel#gmail.com',
subject = 'Sample Email Message',
html_content = f'<strong>From: {self.email}</strong><br><strong>Reason: {self.reason}</strong><br><strong>Message:</strong>{self.message}')
# Set the reply-to email
message.reply_to(self.email)
try:
sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
response = sg.send(message)
except Exception as e:
print(e)
See the examples in the helper library for more detail.

Django Sending Modelform as an email

I created a site where my techs submit their inventory using model forms. Everything is working as intended but I would like to add the function of sending the whole form as an email when they submit their inventory. This would allow for my inventory team to verify counts without having to log in and check the website.
Here is my view.py I know it works if I remove the email bits and saves to my models. Currently returns an error:
'dict' object has no attribute 'splitlines'
form = Inventory_Form()
if request.method == 'POST':
form = Inventory_Form(request.POST)
tech_field = form.save(commit=False)
tech_field.technician = request.user
tech_field.save()
if form.is_valid():
form.save()
name = form.cleaned_data['initials_date']
from_email = 'operations#imbadatthis.com'
subject = 'Weekly Inventory', form.cleaned_data['initials_date']
message = form.cleaned_data
try:
send_mail(subject, message, from_email, ['myemail#n00b.com'], name)
except BadHeaderError:
return HttpResponse('Invalid header found.')
return response, redirect('inventory_submitted')
return render(request, 'inventory.html', {'form': form})
Would it be better to save the form to a csv then attach it as an email? I looked at this and also had issues with that part.
I guess the error is raised at the send_mail because of
message = form.cleaned_data
Because this is a dict and the send_mail from django expects the message to be a string.
You have to convert the dict to a string.
Maybe this helps to make a nice looking email. (documentation)

django rest auth returns "Password reset e-mail has been sent" when it has not, e.g. because email address not registered

I've set up django-rest-auth to send a password reset email. Unfortunately, the API returns a success message "Password reset e-mail has been sent" even when the mail has actually not been sent, for example because there is no user with that email address.
I'm using Mailgun and can see from the logs that the message was not sent.
Is there any way to get django-rest-auth to return a failure message when the email isn't sent? There is an open issue on the GitHub project for this, so I'm not the only person to have seen this problem.
Edit: I have a partial solution but still can't work out how to return a sensible response from the server instead of creating a server error, see below.
https://github.com/Tivix/django-rest-auth/issues/423
I've found the file that generates the response:
venv/lib/python3.6/site-packages/rest_auth/views.py
class PasswordResetView(GenericAPIView):
"""
Calls Django Auth PasswordResetForm save method.
Accepts the following POST parameters: email
Returns the success/fail message.
"""
serializer_class = PasswordResetSerializer
permission_classes = (AllowAny,)
def post(self, request, *args, **kwargs):
# Create a serializer with request.data
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
# Return the success message with OK HTTP status
return Response(
{"detail": _("Password reset e-mail has been sent.")},
status=status.HTTP_200_OK
)
I think this in turn uses a serializer defined in venv/lib/python3.6/site-packages/rest_auth/serializers.py:
class PasswordResetSerializer(serializers.Serializer):
"""
Serializer for requesting a password reset e-mail.
"""
email = serializers.EmailField()
password_reset_form_class = PasswordResetForm
def get_email_options(self):
"""Override this method to change default e-mail options"""
return {}
def validate_email(self, value):
# Create PasswordResetForm with the serializer
self.reset_form = self.password_reset_form_class(data=self.initial_data)
if not self.reset_form.is_valid():
raise serializers.ValidationError(self.reset_form.errors)
return value
def save(self):
request = self.context.get('request')
# Set some values to trigger the send_email method.
opts = {
'use_https': request.is_secure(),
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
'request': request,
}
opts.update(self.get_email_options())
self.reset_form.save(**opts)
I found PasswordResetForm referenced in venv/lib/python3.6/site-packages/django/contrib/auth/views.py:
#csrf_protect
def password_reset(request,
template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html',
subject_template_name='registration/password_reset_subject.txt',
password_reset_form=PasswordResetForm,
token_generator=default_token_generator,
post_reset_redirect=None,
from_email=None,
extra_context=None,
html_email_template_name=None,
extra_email_context=None):
warnings.warn("The password_reset() view is superseded by the "
"class-based PasswordResetView().",
RemovedInDjango21Warning, stacklevel=2)
if post_reset_redirect is None:
post_reset_redirect = reverse('password_reset_done')
else:
post_reset_redirect = resolve_url(post_reset_redirect)
if request.method == "POST":
form = password_reset_form(request.POST)
if form.is_valid():
opts = {
'use_https': request.is_secure(),
'token_generator': token_generator,
'from_email': from_email,
'email_template_name': email_template_name,
'subject_template_name': subject_template_name,
'request': request,
'html_email_template_name': html_email_template_name,
'extra_email_context': extra_email_context,
}
form.save(**opts)
return HttpResponseRedirect(post_reset_redirect)
else:
form = password_reset_form()
context = {
'form': form,
'title': _('Password reset'),
}
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context)
I'm not very familiar with Python - the whole point of using a framework was to avoid writing standard functions such as auth. But it looks like it's just returning a success message without checking that the email can actually be sent.
Edit: after poking around in the allauth code, I have a partial solution. I've updated the form in forms.py like this:
Modified forms.py:
class PasswordResetForm(forms.Form):
email = forms.EmailField(label=_("Email"), max_length=254)
def send_mail(self, subject_template_name, email_template_name,
context, from_email, to_email, html_email_template_name=None):
"""
Send a django.core.mail.EmailMultiAlternatives to `to_email`.
"""
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
if html_email_template_name is not None:
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
email_message.send()
def get_users(self, email):
"""Given an email, return matching user(s) who should receive a reset.
This allows subclasses to more easily customize the default policies
that prevent inactive users and users with unusable passwords from
resetting their password.
"""
active_users = UserModel._default_manager.filter(**{
'%s__iexact' % UserModel.get_email_field_name(): email,
'is_active': True,
})
if not active_users:
raise forms.ValidationError(_("The e-mail address is not assigned to any user account"),
code='invalid')
return (u for u in active_users if u.has_usable_password())
def save(self, domain_override=None,
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
from_email=None, request=None, html_email_template_name=None,
extra_email_context=None):
"""
Generate a one-use only link for resetting password and send it to the
user.
"""
email = self.cleaned_data["email"]
for user in self.get_users(email):
print('user')
print(user)
if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
else:
site_name = domain = domain_override
context = {
'email': email,
'domain': domain,
'site_name': site_name,
'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
'user': user,
'token': token_generator.make_token(user),
'protocol': 'https' if use_https else 'http',
}
if extra_email_context is not None:
context.update(extra_email_context)
self.send_mail(
subject_template_name, email_template_name, context, from_email,
email, html_email_template_name=html_email_template_name,
)
Note the addition of a check which then raises a forms.ValidationError. The problem with this is that the server then throws an error and returns 500. How can I get it to return a handled error response like 400 with a useful message?
I tried adding a custom clean message but still got a 500 server error:
def clean_email(self):
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email)
if not self.users:
raise forms.ValidationError(_("The e-mail address is not assigned"
" to any user account"))
return self.cleaned_data["email"]
Many thanks for any help!
You almost got it work. The thing missing is to use DRF's ValidationError and not Django's:
from rest_framework.exceptions import ValidationError
For more details, see my other answer.

Django e-mail template error

I'm trying to setup a HTML template that takes in some field forms for the subject header, and for part of the content.
views.py
if len(recipient) > 0:
messages.success(request, "Receipt sent successfully!")
subject = "Your Booking Reference: "
to = [recipient]
from_email = 'orders#demo.net'
template = get_template('booking/email/booking_reference.html')
message = EmailMessage(subject, template, from_email, ['test#test.com'])
message.content_subtype = 'html'
message.send()
return HttpResponse("Sent!")
else:
return index(request)
Whenever I request an email to be sent, I get the following error:
'Template' object has no attribute 'encode'
If I comment out message.content_subtype = 'html', I get the desired HttpResponse, but with no e-mail sent. I've added this setting to my settings.py file so that all e-mails get output to the console, but nothing is displayed
settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Try to write this :
message = EmailMultiAlternatives (subject, template, from_email, [test#test.com])
message.attach_alternative(template, "text/html")
Ok I think the problem is that you don't add a context in your template so try this:
if len(recipient) > 0:
messages.success(request, "Receipt sent successfully!")
subject = "Your Booking Reference: "
to = [recipient]
from_email = 'orders#demo.net'
template = loader.get_template('booking/email/booking_reference.html')
context = RequestContext(request, locals())
template = template.render(context)
message = EmailMessage(subject, template, from_email, ['test#test.com'])
message.content_subtype = 'html'
message.send(True)
return HttpResponse("Sent!")
else:
return index(request)

Categories