Django - testing emails with the outbox using post_office - python

I'm writing tests for an application that uses the django-post_office package for most of its email functionality.
The default django.core.mail library contains plenty of useful tools for testing whether or not there are actually emails being sent. (Without actually sending any during the tests)
class TestFunctionThatSendsEmail(Testcase):
#override_settings(
EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend'
)
def test_function_sends_email(self):
self.assertEqual(len(outbox), 0)
run_function_that_calls_email()
self.assertEqual(len(outbox), 1)
...
# other tests
However, the emails in our function are being sent with the django-post_office mail.send() function
# priority now to make sure that they are being sent right away
mail.send(
sender=sender,
recipients=to,
context=context,
template=template_name,
priority='now',
)
Which causes the test above to fail as for some reason the emails do not end up in the outbox.
The strange thing is that if I change the EMAIL_BACKEND to django.core.mail.backends.console.EmailBackend the emails do show up in my terminal, so it is listening to the EMAIL_BACKEND settings.
I've tried finding alternative methods / functions to test this functionality in the django-post_office github, but all I could find was the advice to check to see if the emails are saved to the database and verify their status. (Which I did and works) but the fact that django seems to be unable to detect any emails actually being sent is making me a little bit nervous.
Does anyone know of a way to make emails sent by django-post_office appear in the outbox or, if that is not possible, a way to make sure that they are actually being sent? (beyond checking the database)

The issue is that django-post_office stores the mail backend in the Email object:
class Email(models.Model):
backend_alias = models.CharField(_("Backend alias"), blank=True, default='',
max_length=64)
When dispatch() is called, it uses this backend.
When creating an email, DPO overrides create() to set the backend from def get_available_backends(), which looks up the backend in the settings conf.
This means that using #override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend') won't set the backend_alias in the Email object correctly.
Instead you need to do this manually when you create the object, as per the dispatch test:
def test_dispatch(self):
"""
Ensure that email.dispatch() actually sends out the email
"""
email = Email.objects.create(to=['to#example.com'], from_email='from#example.com',
subject='Test dispatch', message='Message', backend_alias='locmem')
email.dispatch()
self.assertEqual(mail.outbox[0].subject, 'Test dispatch')
If you are using mail.send() you can simply pass the mail.send(backend='locmem') as an argument; make sure you have locmem': 'django.core.mail.backends.locmem.EmailBackend' in your POST_OFFICE settings

I was having the same issue as you described and fixed it by setting the DEFAULT_PRIORITY to 'now':
class TestFunctionThatSendsEmail(Testcase):
#override_settings(
POST_OFFICE={
'BACKENDS': {'default': 'django.core.mail.backends.locmem.EmailBackend'},
'DEFAULT_PRIORITY': 'now',
}
)
def test_function_sends_email(self):
self.assertEqual(len(outbox), 0)
run_function_that_calls_email()
self.assertEqual(len(outbox), 1)
...
# other tests

Related

Django email sending on Heroku

Here is my properties in settings.py file:
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'infobot9#gmail.com'
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
EMAIL_PORT = 587
and here is my send email method:
from django.core.mail import send_mail
def sendConfirmEmail(email, instance, code):
mail_subject = 'Confirmation code {}'.format(code)
message = render_to_string("msg.html", {
'user': instance,
'code': code
})
to_email = email
send_mail(mail_subject, message, 'infobot9#gmail.com', [to_email],
fail_silently=False)
My Django email sending methods work fine in my local host. After deploying it to Heroku I have allowed the login from unknown devices in my Gmail settings. Gmail does not allow the server login to my account and sends me a message:
spicious login attempt blocked
infobot9#gmail.com
Someone tried to log into your account using the password set for them. If it was not you, we recommend that you change your password as soon as possible.

Unknown Device

April 4, 11:39

Near this place: Dublin, Ireland
176.34.163.6 (IP address)
Should I set extra parameters in my settings.py file or I need change my Gmail account settings?
I urge you not to use Gmail for sending email in production. It's not designed for that, and as you've discovered there are measures in place to prevent it from being used as a spam relay. Even if you're sending legitimate email, Gmail is going to make things difficult for you.
Instead, use a service that's designed to send mail from hosted applications like SendGrid or Mailgun. These are both listed among Heroku's addons and both have free starter plans. Pick one and go through its getting sarted guide. Not only will this work better with small volumes of mail, it sets you up nicely for growth.
Allow less secure apps
Display Unlock Captcha
If you still want to use Gmail, #Pierre Monico's answer will work. I just wanted to make an edit. After allowing less secure apps to sign in to your account and Display Unlock Capatcha you should still keep two things in mind. First be sure to be logged into your browser through the account which you are using in your app to send email so that Capatch should be unlocked for that particular account and the second thing is that Google only allows Display Unlock Capatcha for Only 10 minutes . Thus if you want to use it again and again just keep Display Unlock Capatcha page open in your browser and keep it refreshing after sometime.
Also if you have 2 factor authentication enabled then these steps won't work. Those accounts have different procedure.
Above mentioned answers didn't work for me. So here's how I did it. Basically, you need to configure an app password.
Go to your Gmail account
Security > in 'Signing in to Google' section
Turn on 2-Step Verification
Set app password
and finally, need to configure settings.py or .env file(follow's the env),
EMAIL_HOST_USER=your_email#gmail.com
EMAIL_HOST_PASSWORD=generated_app_password
Tip
Using python-decouple makes it much easier to handle .env data

Django password reset not working with django-anymail

I'm trying to implement django-anymail with my site, and am having trouble.
Using these settings:
ANYMAIL = {
"MAILGUN_API_KEY": "key-hahahahahahahahhaha... no",
}
EMAIL_BACKEND = "anymail.backends.mailgun.MailgunBackend"
DEFAULT_FROM_EMAIL = "account-recovery#mg.mycooldomain.com"
This works fine:
send_mail(
"Test it!",
"A new user [%s] has signed up" % (username),
"Accounts <account-creation#mg.mycooldomain.com>",
["my_email#mycooldomain.com",]
)
The mail sends, it shows up in the mailgun logs, all is well.
But trying to use the in-built django registration password reset doesn't do anything.
The logs show no errors, all the templates are in the right place, it says the password reset worked but no emails show up in my inbox, nothing in the mailgun logs and not even anything in the spam box.
No emails show up at all. How can I make this work?
edit:
Also this works, but still no recovery:
from django.core.mail.message import EmailMessage
EmailMessage('hello','hello',recipients="my_email#gmail.com").send(False)
I figured out the answer, and it didn't come up elsewhere.
The call to send_mail worked as it forces an email to be sent.
However, password recovery has a lot of indirection, and one of the checks is if the user has_usable_password, which when you are working with dummy users during testing I forgot to do.
So, make sure your users have a password set before attempting to recover a password.

How to send an email to a user programatically? (OpenERP & Python)

In my OpenERP modules I occasionally need to send notifications or messages to my users, but the obvious way (to me, at least) of using res.users.message_post put a message on the users record instead of sending the message to the user. Any ideas?
Before you can send messages you need to have the Social Network module installed (it lives in .../addons/mail if you want to see its code).
After that, the following snippet will do the job:
from openerp import SUPERUSER_ID
def send_message(oe, cr, user_ids, subject, body):
"sends message to user_ids"
mail_message = oe.pool.get('mail.message')
mail_message_subtype = oe.pool.get('mail.message.subtype')
[discussion_id] = mail_message_subtype.browse(
cr, SUPERUSER_ID, [('name','=','Discussions')]
)
users = oe.pool.get('res.users').search(cr, uid, user_ids)
mail_message.create(cr, SUPERUSER_ID, values=dict(
type='email',
subtype_id=discussion.id,
partner_ids=[(4, u.partner_id.id) for u in users],
subject=subject,
body=body,
))
Stuff this in a utility module somewhere and you're good to go.
A couple comments:
When calling, pass self as the first parameter
send_message(self, cr, ...)
if you want to make this a method of a class instead, just replace the oe's with self, and omit self from the parameters
This will send a message inside OpenERP to the users and, if any user has his/her preferences set up to forward to external email, an actual email will be generated for them.

Django detect errors while sending mail

def send_one_mail(request):
send_mail('The subject', 'The body to testing', 'userone#example.com',['usertwo#gmail.com'], fail_silently=False)
return HttpResponse("This mail has sent successfull")
And my settings.py is
EMAIL_USE_TLS = True
EMAIL_HOST = 'mail.example.com'
EMAIL_HOST_USER = 'userone#example.com'
EMAIL_HOST_PASSWORD = 'password'
EMAIL_PORT = 587
DEFAULT_FROM_EMAIL = 'userone#example.com'
SERVER_EMAIL = 'userone#example.com'
I have a view (which works) as shown above, here userone#example.com is sender and usertwo#gmail.com
is the receiver.
How can i check and act according to errors in sending mail?
(I want to show "Some error occured" in such errors)
How can i determine if the usertwo#gmail.com(reciver) exists or not?
(if email can't be reached, i want to show the user that the email doesn't exist)
I am using postfix,dovecot etc. in server.
You can pick up some errors when you use fail_silently = False. Just wrap send_mail in try/except.
When i wanted more control over e-mail sending, then i stopped using django mailing completely and installed lamson instead (lamsonproject.org). You can basically create your own mail server with it, attach your django orm to it and provide detailed feedback about what has happened to your e-mails. If you insert some kind of downloadable content into those e-mails (like images), then you can even give hashes to images and verify this way if e-mail has been opened too. You could do that with django based email sending too. Lamson just gives bit more control over the what and how that goes on after you hit send button.
django-mail-queue
Anyway, i finally ended-up with using the below package.
pip install django-mail-queue
And here is the docs
https://django-mail-queue.readthedocs.org/en/latest/

Avoid Mandrill errors with Sentry

I have installed a Sentry instance on my server, and I want to configure it to send alert emails using Mandrill and its Django integration djrill. Here's the relevant settings in the sentry.conf.py file I'm using:
EXTRA_INSTALLED_APPS = (
'djrill',
)
EMAIL_BACKEND = 'djrill.mail.backends.djrill.DjrillBackend'
MANDRILL_API_KEY = '[... Mandril API key ...]'
DEFAULT_FROM_EMAIL = 'my-mandrill-allowed#email.address'
SERVER_EMAIL = 'my-mandrill-allowed#email.address'
And this setup works, except for the part that for some reason Mandrill doesn't allow setting the Message-Id header:
NotSupportedByMandrillError: Invalid message header 'Message-Id' - Mandrill only allows Reply-To and X-* headers
(This exception is raised by djrill, is not a response from Mandrill)
Which is set by Sentry here:
class MessageBuilder(object):
# ...
#cached_property
def message_id(self):
if self.reference is not None:
return email_id_for_model(self.reference)
I have managed to make it work by editing that method and make it always return None, so no Message-Id header is set in the email. But I don't like to edit/patch 3rd party code and I have no idea if that header is needed elsewhere.
How to accomplish this correctly? Switching from Mandrill is not an option right now.
Thanks!
As you can't easily change Sentry's behavior, as far as I can tell, I'd suggest implementing a subclass of DjrillBackend that removes the Message-Id header before the messages are sent. Something like (untested):
class HeaderRemovingBackend(DjrillBackend):
def send_messages(self, email_messages):
for message in email_messages:
if 'Message-Id' in message.extra_headers:
del message.extra_headers['Message-Id']
super(HeaderRemovingBackend, self).send_messages(email_messages)

Categories