Repository: mdn/kuma

Issue:
SMTPServerDisconnected sending confirmation email
https://sentry.prod.mozaws.net/operations/mdn-prod/issues/7634296/

```
Resolver404: {'tried': [[<URLPattern '^media/(?:redesign/)?css/(?P<doc>.*)-min.css$'>], [<URLPattern '^media/(?:redesign/)?js/(?P<doc>.*)-min.js$'>], [<URLPattern '^media/(?:redesign/)?img(?P<suffix>.*)$'>], [<URLPattern '^media/(?:redesign/)?css(?P<suffix>.*)$'>], [<URLPattern '^media/(?:redesign/)?js(?P<suffix>.*)$'>], [<URLPattern '^media/(?:redesign/)?fonts(?P<suffix>.*)$'>], [<URLPattern '^media/uploads/demos/(?:.*)$'>], [<URLPattern '(?i)^(?P<one>.*)//(?P<two>.*)//(?P<three>.*)$'>], [<URLPattern '(?i)^(?P<one>.*)//(?P<two>.*)$'>], [<URLPattern '(?i)^samples/canvas-tutorial/2_1_canvas_rect.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/2_2_canvas_moveto.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/2_3_canvas_lineto.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/2_4_canvas_arc.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/2_5_canvas_quadraticcurveto.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/2_6_canvas_beziercurveto.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/3_1_canvas_drawimage.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/3_2_canvas_drawimage.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/3_3_canvas_drawimage.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/3_4_canvas_gallery.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_1_canvas_fillstyle.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_2_canvas_strokestyle.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_3_canvas_globalalpha.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_4_canvas_rgba.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_5_canvas_linewidth.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_6_canvas_linecap.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_7_canvas_linejoin.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_8_canvas_miterlimit.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_9_canvas_lineargradient.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_10_canvas_radialgradient.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/4_11_canvas_createpattern.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/5_1_canvas_savestate.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/5_2_canvas_translate.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/5_3_canvas_rotate.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/5_4_canvas_scale.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/6_1_canvas_composite.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/6_2_canvas_clipping.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/globalCompositeOperation.html$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/backdrop.png$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/bg_gallery.png$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_1.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_2.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_3.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_4.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_5.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_6.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_7.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/gallery_8.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/picture_frame.png$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/rhino.jpg$'>], [<URLPattern '(?i)^samples/canvas-tutorial/images/wallpaper.png$'>], [<URLPattern '(?i)^samples/domref/mozGetAsFile.html$'>], [<URLPattern '(?i)^samples/raycaster/input.js$'>], [<URLPattern '(?i)^samples/raycaster/Level.js$'>], [<URLPattern '(?i)^samples/raycaster/Player.js$'>], [<URLPattern '(?i)^samples/raycaster/RayCaster.html$'>], [<URLPattern '(?i)^samples/raycaster/RayCaster.js$'>], [<URLPattern '(?i)^samples/raycaster/trace.css$'>], [<URLPattern '(?i)^samples/raycaster/trace.js$'>], [<URLPattern '(?i)^samples/webgl/sample1$'>], [<URLPattern '(?i)^samples/webgl/sample1/index.html$'>], [<URLPattern '(?i)^samples/webgl/sample1/webgl-demo.js$'>], [<UR...
  File "redirect_urls/middleware.py", line 14, in __call__
    resolver_match = self.resolver.resolve(request.path_info)
  File "newrelic/hooks/framework_django.py", line 600, in wrapper
    return _wrapped(*args, **kwargs)
  File "newrelic/hooks/framework_django.py", line 588, in _wrapped
    result = wrapped(path)
  File "django/urls/resolvers.py", line 567, in resolve
    raise Resolver404({'tried': tried, 'path': new_path})

SMTPServerDisconnected: Connection unexpectedly closed
(21 additional frame(s) were not displayed)
...
  File "allauth/account/managers.py", line 19, in add_email
    email_address.send_confirmation(request, signup=signup)
  File "allauth/account/models.py", line 58, in send_confirmation
    confirmation.send(request, signup=signup)
  File "allauth/account/models.py", line 163, in send
    get_adapter(request).send_confirmation_mail(request, self, signup)
  File "allauth/account/adapter.py", line 448, in send_confirmation_mail
    self.send_mail(email_template,
  File "allauth/account/adapter.py", line 137, in send_mail
    msg.send()

SMTPServerDisconnected: Connection unexpectedly closed
```


Discussion:
[This commit](https://github.com/mdn/kuma/commit/1a4e8dbf7d9b036e13a0244afd60962e08698c43) is in `master` but it's not in production yet. At least wasn't when this Sentry event happened. 

However, it's not clear if that commit would have fixed *this* error. 

<details><summary>Full traceback</summary>
SMTPServerDisconnected: Connection unexpectedly closed
  File "django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "newrelic/hooks/framework_django.py", line 539, in wrapper
    return wrapped(*args, **kwargs)
  File "django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "django/views/generic/base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "allauth/account/views.py", line 383, in dispatch
    return super(EmailView, self).dispatch(request, *args, **kwargs)
  File "newrelic/hooks/framework_django.py", line 930, in wrapper
    return wrapped(*args, **kwargs)
  File "django/views/generic/base.py", line 97, in dispatch
    return handler(request, *args, **kwargs)
  File "allauth/account/views.py", line 407, in post
    res = super(EmailView, self).post(request, *args, **kwargs)
  File "allauth/account/views.py", line 104, in post
    response = self.form_valid(form)
  File "allauth/account/views.py", line 391, in form_valid
    email_address = form.save(self.request)
  File "allauth/account/forms.py", line 448, in save
    return EmailAddress.objects.add_email(request,
  File "allauth/account/managers.py", line 19, in add_email
    email_address.send_confirmation(request, signup=signup)
  File "allauth/account/models.py", line 58, in send_confirmation
    confirmation.send(request, signup=signup)
  File "allauth/account/models.py", line 163, in send
    get_adapter(request).send_confirmation_mail(request, self, signup)
  File "allauth/account/adapter.py", line 448, in send_confirmation_mail
    self.send_mail(email_template,
  File "allauth/account/adapter.py", line 137, in send_mail
    msg.send()
  File "newrelic/api/function_trace.py", line 151, in literal_wrapper
    return wrapped(*args, **kwargs)
  File "django/core/mail/message.py", line 291, in send
    return self.get_connection(fail_silently).send_messages([self])
  File "django/core/mail/backends/smtp.py", line 103, in send_messages
    new_conn_created = self.open()
  File "django/core/mail/backends/smtp.py", line 63, in open
    self.connection = self.connection_class(self.host, self.port, **connection_params)
  File "python3.8/smtplib.py", line 253, in __init__
    (code, msg) = self.connect(host, port)
  File "python3.8/smtplib.py", line 341, in connect
    (code, msg) = self.getreply()
  File "python3.8/smtplib.py", line 398, in getreply
    raise SMTPServerDisconnected("Connection unexpectedly closed")
</details>
In particular, the sending seems to be happening deep inside `allauth` but we're overriding some of those classes so we should be able to use `redo` in some way so that any `SMTPServerDisconnected` that happens out of reach are simply retried. 
This error keeps recurring, can we reevaluate?

Resolving patch:
diff --git a/kuma/core/utils.py b/kuma/core/utils.py
--- a/kuma/core/utils.py
+++ b/kuma/core/utils.py
@@ -599,6 +599,7 @@ def send(self, *args, retry_options=None, **kwargs):
             "retry_exceptions": (SMTPServerDisconnected,),
             # The default in redo is 60 seconds. Let's tone that down.
             "sleeptime": 3,
+            "attempts": 10,
         }
 
         parent_method = super(EmailMultiAlternativesRetrying, self).send
diff --git a/kuma/users/adapters.py b/kuma/users/adapters.py
--- a/kuma/users/adapters.py
+++ b/kuma/users/adapters.py
@@ -1,3 +1,5 @@
+from smtplib import SMTPServerDisconnected
+
 from allauth.account.adapter import DefaultAccountAdapter, get_adapter
 from allauth.account.models import EmailAddress
 from allauth.account.utils import cleanup_email_addresses
@@ -11,6 +13,7 @@
 from django.shortcuts import redirect, render
 from django.utils.cache import add_never_cache_headers
 from django.utils.translation import gettext_lazy as _
+from redo import retrying
 from waffle import switch_is_active
 
 from kuma.core.ga_tracking import (
@@ -139,6 +142,17 @@ def save_user(self, request, user, form, commit=True):
             user.save()
         return user
 
+    def send_mail(self, *args, **kwargs):
+        retry_options = {
+            "retry_exceptions": (SMTPServerDisconnected,),
+            # The default in redo is 60 seconds. Let's tone that down.
+            "sleeptime": 3,
+            "attempts": 10,
+        }
+        parent_method = super(KumaAccountAdapter, self).send_mail
+        with retrying(parent_method, **retry_options) as method:
+            return method(*args, **kwargs)
+
 
 class KumaSocialAccountAdapter(DefaultSocialAccountAdapter):
     def is_open_for_signup(self, request, sociallogin):


Test patch: