diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index d29480648e6b902118a8c77c04dd0ff063cec3b1..1372f59e3ca362057f1e95d1cb1d4adf79598762 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -144,7 +144,6 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
 MIDDLEWARE = (
     # Make sure djangosecure.middleware.SecurityMiddleware is listed first
     'django.contrib.sessions.middleware.SessionMiddleware',
-    'funkwhale_api.users.middleware.AnonymousSessionMiddleware',
     'corsheaders.middleware.CorsMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
@@ -173,7 +172,10 @@ FIXTURE_DIRS = (
 
 # EMAIL CONFIGURATION
 # ------------------------------------------------------------------------------
-EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
+EMAIL_CONFIG = env.email_url(
+    'EMAIL_CONFIG', default='consolemail://')
+
+vars().update(EMAIL_CONFIG)
 
 # DATABASE CONFIGURATION
 # ------------------------------------------------------------------------------
@@ -293,7 +295,7 @@ AUTHENTICATION_BACKENDS = (
     'django.contrib.auth.backends.ModelBackend',
     'allauth.account.auth_backends.AuthenticationBackend',
 )
-
+SESSION_COOKIE_HTTPONLY = False
 # Some really nice defaults
 ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
 ACCOUNT_EMAIL_REQUIRED = True
@@ -368,6 +370,7 @@ CORS_ORIGIN_ALLOW_ALL = True
 #     'funkwhale.localhost',
 # )
 CORS_ALLOW_CREDENTIALS = True
+
 REST_FRAMEWORK = {
     'DEFAULT_PERMISSION_CLASSES': (
         'rest_framework.permissions.IsAuthenticated',
@@ -392,6 +395,11 @@ REST_FRAMEWORK = {
         'django_filters.rest_framework.DjangoFilterBackend',
     )
 }
+REST_AUTH_SERIALIZERS = {
+    'PASSWORD_RESET_SERIALIZER': 'funkwhale_api.users.serializers.PasswordResetSerializer'  # noqa
+}
+REST_SESSION_LOGIN = False
+REST_USE_JWT = True
 
 ATOMIC_REQUESTS = False
 USE_X_FORWARDED_HOST = True
diff --git a/api/config/settings/local.py b/api/config/settings/local.py
index dcbea66d26134664655d1ca0978e3121c5da5d96..59260062985cf1e3c554bfe4459d237f5d981e11 100644
--- a/api/config/settings/local.py
+++ b/api/config/settings/local.py
@@ -25,9 +25,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY", default='mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0
 # ------------------------------------------------------------------------------
 EMAIL_HOST = 'localhost'
 EMAIL_PORT = 1025
-EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND',
-                    default='django.core.mail.backends.console.EmailBackend')
-
 
 # django-debug-toolbar
 # ------------------------------------------------------------------------------
diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py
index 762d5bf7b2cf66bdd9a96325c630db65a53ddaae..480461d35ed60fc0629e417879b6f75e27a95418 100644
--- a/api/funkwhale_api/history/models.py
+++ b/api/funkwhale_api/history/models.py
@@ -21,13 +21,6 @@ class Listening(models.Model):
     class Meta:
         ordering = ('-creation_date',)
 
-    def save(self, **kwargs):
-        if not self.user and not self.session_key:
-            raise ValidationError('Cannot have both session_key and user empty for listening')
-
-        super().save(**kwargs)
-
-
     def get_activity_url(self):
         return '{}/listenings/tracks/{}'.format(
             self.user.get_activity_url(), self.pk)
diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py
index 8fe6fa6e01f07a395f2c337ea45591bd315a03d3..f7333f24349777a6a41b15e4de0d6a282502aacd 100644
--- a/api/funkwhale_api/history/serializers.py
+++ b/api/funkwhale_api/history/serializers.py
@@ -36,13 +36,9 @@ class ListeningSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = models.Listening
-        fields = ('id', 'user', 'session_key', 'track', 'creation_date')
-
+        fields = ('id', 'user', 'track', 'creation_date')
 
     def create(self, validated_data):
-        if self.context.get('user'):
-            validated_data['user'] = self.context.get('user')
-        else:
-            validated_data['session_key'] = self.context['session_key']
+        validated_data['user'] = self.context['user']
 
         return super().create(validated_data)
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index d5cbe316ba88b455755cdbaf843086f7c99c09f3..bea96a4187792421bf6827717eb1032e9dcaacf3 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -1,4 +1,5 @@
 from rest_framework import generics, mixins, viewsets
+from rest_framework import permissions
 from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.decorators import detail_route
@@ -10,31 +11,26 @@ from funkwhale_api.music.serializers import TrackSerializerNested
 from . import models
 from . import serializers
 
-class ListeningViewSet(mixins.CreateModelMixin,
-                          mixins.RetrieveModelMixin,
-                          viewsets.GenericViewSet):
+
+class ListeningViewSet(
+        mixins.CreateModelMixin,
+        mixins.RetrieveModelMixin,
+        viewsets.GenericViewSet):
 
     serializer_class = serializers.ListeningSerializer
     queryset = models.Listening.objects.all()
-    permission_classes = [ConditionalAuthentication]
+    permission_classes = [permissions.IsAuthenticated]
 
     def perform_create(self, serializer):
         r = super().perform_create(serializer)
-        if self.request.user.is_authenticated:
-            record.send(serializer.instance)
+        record.send(serializer.instance)
         return r
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        if self.request.user.is_authenticated:
-            return queryset.filter(user=self.request.user)
-        else:
-            return queryset.filter(session_key=self.request.session.session_key)
+        return queryset.filter(user=self.request.user)
 
     def get_serializer_context(self):
         context = super().get_serializer_context()
-        if self.request.user.is_authenticated:
-            context['user'] = self.request.user
-        else:
-            context['session_key'] = self.request.session.session_key
+        context['user'] = self.request.user
         return context
diff --git a/api/funkwhale_api/radios/models.py b/api/funkwhale_api/radios/models.py
index 0273b53871b4536a52b34870ef5ba2e1e93ae078..8758abc619d05c03b3fe9c9b0953c919ef005479 100644
--- a/api/funkwhale_api/radios/models.py
+++ b/api/funkwhale_api/radios/models.py
@@ -55,8 +55,6 @@ class RadioSession(models.Model):
     related_object = GenericForeignKey('related_object_content_type', 'related_object_id')
 
     def save(self, **kwargs):
-        if not self.user and not self.session_key:
-            raise ValidationError('Cannot have both session_key and user empty for radio session')
         self.radio.clean(self)
         super().save(**kwargs)
 
diff --git a/api/funkwhale_api/radios/serializers.py b/api/funkwhale_api/radios/serializers.py
index 2e7e6a409fb4e4102f1d713faaeb94ca3b7e759a..195b382c99a4b32872b34923f958e1c8b682ac94 100644
--- a/api/funkwhale_api/radios/serializers.py
+++ b/api/funkwhale_api/radios/serializers.py
@@ -38,6 +38,7 @@ class RadioSerializer(serializers.ModelSerializer):
 
         return super().save(**kwargs)
 
+
 class RadioSessionTrackSerializerCreate(serializers.ModelSerializer):
     class Meta:
         model = models.RadioSessionTrack
@@ -62,17 +63,14 @@ class RadioSessionSerializer(serializers.ModelSerializer):
             'user',
             'creation_date',
             'custom_radio',
-            'session_key')
+        )
 
     def validate(self, data):
         registry[data['radio_type']]().validate_session(data, **self.context)
         return data
 
     def create(self, validated_data):
-        if self.context.get('user'):
-            validated_data['user'] = self.context.get('user')
-        else:
-            validated_data['session_key'] = self.context['session_key']
+        validated_data['user'] = self.context['user']
         if validated_data.get('related_object_id'):
             radio = registry[validated_data['radio_type']]()
             validated_data['related_object'] = radio.get_related_object(validated_data['related_object_id'])
diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py
index ffd1d16593ddc2648ec2748dc3b4174afe41f7ed..37c07c5e4cdf7c30799aa8b3af12fd94d1150274 100644
--- a/api/funkwhale_api/radios/views.py
+++ b/api/funkwhale_api/radios/views.py
@@ -2,6 +2,7 @@ from django.db.models import Q
 from django.http import Http404
 
 from rest_framework import generics, mixins, viewsets
+from rest_framework import permissions
 from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.decorators import detail_route, list_route
@@ -24,7 +25,7 @@ class RadioViewSet(
         viewsets.GenericViewSet):
 
     serializer_class = serializers.RadioSerializer
-    permission_classes = [ConditionalAuthentication]
+    permission_classes = [permissions.IsAuthenticated]
     filter_class = filtersets.RadioFilter
 
     def get_queryset(self):
@@ -84,21 +85,15 @@ class RadioSessionViewSet(mixins.CreateModelMixin,
 
     serializer_class = serializers.RadioSessionSerializer
     queryset = models.RadioSession.objects.all()
-    permission_classes = [ConditionalAuthentication]
+    permission_classes = [permissions.IsAuthenticated]
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        if self.request.user.is_authenticated:
-            return queryset.filter(user=self.request.user)
-        else:
-            return queryset.filter(session_key=self.request.session.session_key)
+        return queryset.filter(user=self.request.user)
 
     def get_serializer_context(self):
         context = super().get_serializer_context()
-        if self.request.user.is_authenticated:
-            context['user'] = self.request.user
-        else:
-            context['session_key'] = self.request.session.session_key
+        context['user'] = self.request.user
         return context
 
 
@@ -106,17 +101,14 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin,
                                viewsets.GenericViewSet):
     serializer_class = serializers.RadioSessionTrackSerializer
     queryset = models.RadioSessionTrack.objects.all()
-    permission_classes = [ConditionalAuthentication]
+    permission_classes = [permissions.IsAuthenticated]
 
     def create(self, request, *args, **kwargs):
         serializer = self.get_serializer(data=request.data)
         serializer.is_valid(raise_exception=True)
         session = serializer.validated_data['session']
         try:
-            if request.user.is_authenticated:
-                assert request.user == session.user
-            else:
-                assert request.session.session_key == session.session_key
+            assert request.user == session.user
         except AssertionError:
             return Response(status=status.HTTP_403_FORBIDDEN)
         track = session.radio.pick()
diff --git a/api/funkwhale_api/templates/account/email/email_confirmation_message.txt b/api/funkwhale_api/templates/account/email/email_confirmation_message.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8aec540fe15c602e99505231e2292cb09839930c
--- /dev/null
+++ b/api/funkwhale_api/templates/account/email/email_confirmation_message.txt
@@ -0,0 +1,8 @@
+{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}!
+
+You're receiving this e-mail because user {{ user_display }} at {{ site_domain }} has given yours as an e-mail address to connect their account.
+
+To confirm this is correct, go to {{ funkwhale_url }}/auth/email/confirm?key={{ key }}
+{% endblocktrans %}{% endautoescape %}
+{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you from {{ site_name }}!
+{{ site_domain }}{% endblocktrans %}
diff --git a/api/funkwhale_api/templates/registration/password_reset_email.html b/api/funkwhale_api/templates/registration/password_reset_email.html
new file mode 100644
index 0000000000000000000000000000000000000000..7a587d7204b1ae31eecc995bcca3d9a7e68a0e4d
--- /dev/null
+++ b/api/funkwhale_api/templates/registration/password_reset_email.html
@@ -0,0 +1,12 @@
+{% load i18n %}{% autoescape off %}
+{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
+
+{% trans "Please go to the following page and choose a new password:" %}
+{{ funkwhale_url }}/auth/password/reset/confirm?uid={{ uid }}&token={{ token }}
+{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
+
+{% trans "Thanks for using our site!" %}
+
+{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
+
+{% endautoescape %}
diff --git a/api/funkwhale_api/users/adapters.py b/api/funkwhale_api/users/adapters.py
index 96d1b8b1d6b4aaa708bc3c09490beae84c10fa1a..7bd341d14e07062376dbc22a16559a6ff23d9321 100644
--- a/api/funkwhale_api/users/adapters.py
+++ b/api/funkwhale_api/users/adapters.py
@@ -1,5 +1,6 @@
-from allauth.account.adapter import DefaultAccountAdapter
+from django.conf import settings
 
+from allauth.account.adapter import DefaultAccountAdapter
 from dynamic_preferences.registries import global_preferences_registry
 
 
@@ -8,3 +9,7 @@ class FunkwhaleAccountAdapter(DefaultAccountAdapter):
     def is_open_for_signup(self, request):
         manager = global_preferences_registry.manager()
         return manager['users__registration_enabled']
+
+    def send_mail(self, template_prefix, email, context):
+        context['funkwhale_url'] = settings.FUNKWHALE_URL
+        return super().send_mail(template_prefix, email, context)
diff --git a/api/funkwhale_api/users/middleware.py b/api/funkwhale_api/users/middleware.py
deleted file mode 100644
index e3eba95f3ff5813b7eab16a633ccb1121e8851f3..0000000000000000000000000000000000000000
--- a/api/funkwhale_api/users/middleware.py
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-class AnonymousSessionMiddleware:
-    def __init__(self, get_response):
-        self.get_response = get_response
-
-    def __call__(self, request):
-        if not request.session.session_key:
-            request.session.save()
-        response = self.get_response(request)
-        return response
diff --git a/api/funkwhale_api/users/rest_auth_urls.py b/api/funkwhale_api/users/rest_auth_urls.py
index 31f5384aa7f2a750bcaa4fc9063658876fbbd968..fa6c425cc5227f8f9d4078338ff436ccc8e883d4 100644
--- a/api/funkwhale_api/users/rest_auth_urls.py
+++ b/api/funkwhale_api/users/rest_auth_urls.py
@@ -1,16 +1,20 @@
 from django.views.generic import TemplateView
 from django.conf.urls import url
 
-from rest_auth.registration.views import VerifyEmailView
-from rest_auth.views import PasswordChangeView
+from rest_auth.registration import views as registration_views
+from rest_auth import views as rest_auth_views
 
-from .views import RegisterView
+from . import views
 
 
 urlpatterns = [
-    url(r'^$', RegisterView.as_view(), name='rest_register'),
-    url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'),
-    url(r'^change-password/$', PasswordChangeView.as_view(), name='change_password'),
+    url(r'^$', views.RegisterView.as_view(), name='rest_register'),
+    url(r'^verify-email/$',
+        registration_views.VerifyEmailView.as_view(),
+        name='rest_verify_email'),
+    url(r'^change-password/$',
+        rest_auth_views.PasswordChangeView.as_view(),
+        name='change_password'),
 
     # This url is used by django-allauth and empty TemplateView is
     # defined just to allow reverse() call inside app, for example when email
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index b21aa69355b2ca4acea883b52e9401055382b6b3..eadce6154fa12c385f0655d3667b1634442716ec 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -1,5 +1,7 @@
-from rest_framework import serializers
+from django.conf import settings
 
+from rest_framework import serializers
+from rest_auth.serializers import PasswordResetSerializer as PRS
 from funkwhale_api.activity import serializers as activity_serializers
 
 from . import models
@@ -63,3 +65,12 @@ class UserReadSerializer(serializers.ModelSerializer):
                 'status': o.has_perm(internal_codename)
             }
         return perms
+
+
+class PasswordResetSerializer(PRS):
+    def get_email_options(self):
+        return {
+            'extra_email_context': {
+                'funkwhale_url': settings.FUNKWHALE_URL
+            }
+        }
diff --git a/api/setup.cfg b/api/setup.cfg
index a2b8b92c682696a0ad4569cb6c9f9e25c01f9b9f..b1267c904cc94dc623dddf93c9af1293ec869122 100644
--- a/api/setup.cfg
+++ b/api/setup.cfg
@@ -11,7 +11,7 @@ python_files = tests.py test_*.py *_tests.py
 testpaths = tests
 env =
     SECRET_KEY=test
-    DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
+    EMAIL_CONFIG=consolemail://
     CELERY_BROKER_URL=memory://
     CELERY_TASK_ALWAYS_EAGER=True
     CACHEOPS_ENABLED=False
diff --git a/api/tests/history/test_history.py b/api/tests/history/test_history.py
index 563cf2f08569327455fc64a5855c8b06bc5d6aa9..20272559636119d51e066d7ebe0e97fd944a5fc2 100644
--- a/api/tests/history/test_history.py
+++ b/api/tests/history/test_history.py
@@ -14,21 +14,6 @@ def test_can_create_listening(factories):
     l = models.Listening.objects.create(user=user, track=track)
 
 
-def test_anonymous_user_can_create_listening_via_api(
-        client, factories, preferences):
-    preferences['common__api_authentication_required'] = False
-    track = factories['music.Track']()
-    url = reverse('api:v1:history:listenings-list')
-    response = client.post(url, {
-        'track': track.pk,
-    })
-
-    listening = models.Listening.objects.latest('id')
-
-    assert listening.track == track
-    assert listening.session_key == client.session.session_key
-
-
 def test_logged_in_user_can_create_listening_via_api(
         logged_in_client, factories, activity_muted):
     track = factories['music.Track']()
diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py
index e78bd0e2fda548ebc204545986536bf2f63f7ff5..b166b648c574f8050f574151aa08e9f3282b49ab 100644
--- a/api/tests/radios/test_radios.py
+++ b/api/tests/radios/test_radios.py
@@ -151,20 +151,6 @@ def test_can_start_radio_for_logged_in_user(logged_in_client):
     assert session.user == logged_in_client.user
 
 
-def test_can_start_radio_for_anonymous_user(api_client, db, preferences):
-    preferences['common__api_authentication_required'] = False
-    url = reverse('api:v1:radios:sessions-list')
-    response = api_client.post(url, {'radio_type': 'random'})
-
-    assert response.status_code == 201
-
-    session = models.RadioSession.objects.latest('id')
-
-    assert session.radio_type == 'random'
-    assert session.user is None
-    assert session.session_key == api_client.session.session_key
-
-
 def test_can_get_track_for_session_from_api(factories, logged_in_client):
     files = factories['music.TrackFile'].create_batch(1)
     tracks = [f.track for f in files]
@@ -227,25 +213,25 @@ def test_can_start_tag_radio(factories):
 
     radio = radios.TagRadio()
     session = radio.start_session(user, related_object=tag)
-    assert session.radio_type =='tag'
+    assert session.radio_type == 'tag'
     for i in range(5):
         assert radio.pick() in good_tracks
 
 
-def test_can_start_artist_radio_from_api(api_client, preferences, factories):
-    preferences['common__api_authentication_required'] = False
+def test_can_start_artist_radio_from_api(
+        logged_in_api_client, preferences, factories):
     artist = factories['music.Artist']()
     url = reverse('api:v1:radios:sessions-list')
 
-    response = api_client.post(
+    response = logged_in_api_client.post(
         url, {'radio_type': 'artist', 'related_object_id': artist.id})
 
     assert response.status_code == 201
 
     session = models.RadioSession.objects.latest('id')
 
-    assert session.radio_type, 'artist'
-    assert session.related_object, artist
+    assert session.radio_type == 'artist'
+    assert session.related_object == artist
 
 
 def test_can_start_less_listened_radio(factories):
@@ -257,6 +243,6 @@ def test_can_start_less_listened_radio(factories):
     good_tracks = [f.track for f in good_files]
     radio = radios.LessListenedRadio()
     session = radio.start_session(user)
-    assert session.related_object == user
+
     for i in range(5):
         assert radio.pick() in good_tracks
diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py
index 4be586965f8d5f02bac7cfc9d3c9b871e2d8fd31..985a78c8a65ed49853869ed18c2cb82a5b2a95db 100644
--- a/api/tests/users/test_views.py
+++ b/api/tests/users/test_views.py
@@ -136,6 +136,20 @@ def test_changing_password_updates_secret_key(logged_in_client):
     assert user.password != password
 
 
+def test_can_request_password_reset(
+        factories, api_client, mailoutbox):
+    user = factories['users.User']()
+    payload = {
+        'email': user.email,
+    }
+    emails = len(mailoutbox)
+    url = reverse('rest_password_reset')
+
+    response = api_client.post(url, payload)
+    assert response.status_code == 200
+    assert len(mailoutbox) > emails
+
+
 def test_user_can_patch_his_own_settings(logged_in_api_client):
     user = logged_in_api_client.user
     payload = {
diff --git a/changes/changelog.d/187.feature b/changes/changelog.d/187.feature
new file mode 100644
index 0000000000000000000000000000000000000000..501331a19327a2b177e62fefeb5e5a5f47f35c5d
--- /dev/null
+++ b/changes/changelog.d/187.feature
@@ -0,0 +1,24 @@
+Users can now request password reset by email, assuming
+a SMTP server was correctly configured (#187)
+
+Update
+^^^^^^
+
+Starting from this release, Funkwhale will send two types
+of emails:
+
+- Email confirmation emails, to ensure a user's email is valid
+- Password reset emails, enabling user to reset their password without an admin's intervention
+
+Email sending is disabled by default, as it requires additional configuration.
+In this mode, emails are simply outputed on stdout.
+
+If you want to actually send those emails to your users, you should edit your
+.env file and tweak the EMAIL_CONFIG variable. See :ref:`setting-EMAIL_CONFIG`
+for more details.
+
+.. note::
+
+  As a result of these changes, the DJANGO_EMAIL_BACKEND variable,
+  which was not documented, has no effect anymore. You can safely remove it from
+  your .env file if it is set.
diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample
index f1718ff7e28f757f8bc993e5a32e22b8f41f9aa2..dfd17ff4d6c5702a9faf9e781c94f1fb622f2fb0 100644
--- a/deploy/env.prod.sample
+++ b/deploy/env.prod.sample
@@ -6,6 +6,7 @@
 # - DJANGO_SECRET_KEY
 # - DJANGO_ALLOWED_HOSTS
 # - FUNKWHALE_URL
+# - EMAIL_CONFIG (if you plan to send emails)
 # On non-docker setup **only**, you'll also have to tweak/uncomment those variables:
 # - DATABASE_URL
 # - CACHE_URL
@@ -41,6 +42,16 @@ FUNKWHALE_API_PORT=5000
 # your instance
 FUNKWHALE_URL=https://yourdomain.funwhale
 
+# Configure email sending using this variale
+# By default, funkwhale will output emails sent to stdout
+# here are a few examples for this setting
+# EMAIL_CONFIG=consolemail://         # output emails to console (the default)
+# EMAIL_CONFIG=dummymail://          # disable email sending completely
+# On a production instance, you'll usually want to use an external SMTP server:
+# EMAIL_CONFIG=smtp://user@:password@youremail.host:25'
+# EMAIL_CONFIG=smtp+ssl://user@:password@youremail.host:465'
+# EMAIL_CONFIG=smtp+tls://user@:password@youremail.host:587'
+
 # Depending on the reverse proxy used in front of your funkwhale instance,
 # the API will use different kind of headers to serve audio files
 # Allowed values: nginx, apache2
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 1c89feeb8d1a244d3e9aa80e0e99cac416053a33..f498b9c87df898380df1c7db586531579165dc49 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -39,6 +39,24 @@ settings in this interface.
 Configuration reference
 -----------------------
 
+.. _setting-EMAIL_CONFIG:
+
+``EMAIL_CONFIG``
+^^^^^^^^^^^^^^^^
+
+Determine how emails are sent.
+
+Default: ``consolemail://``
+
+Possible values:
+
+- ``consolemail://``: Output sent emails to stdout
+- ``dummymail://``: Completely discard sent emails
+- ``smtp://user:password@youremail.host:25``: Send emails via SMTP via youremail.host on port 25, without encryption, authenticating as user "user" with password "password"
+- ``smtp+ssl://user:password@youremail.host:465``: Send emails via SMTP via youremail.host on port 465, using SSL encryption, authenticating as user "user" with password "password"
+- ``smtp+tls://user:password@youremail.host:587``: Send emails via SMTP via youremail.host on port 587, using TLS encryption, authenticating as user "user" with password "password"
+
+
 .. _setting-MUSIC_DIRECTORY_PATH:
 
 ``MUSIC_DIRECTORY_PATH``
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index 8e0df801656bdd761a82396c5a66178b7e730c1c..97c743bbe804a5ca64bfe39ce69ad5b725745365 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -35,24 +35,24 @@
           <div class="header">{{ $t('My account') }}</div>
           <div class="menu">
             <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><i class="user icon"></i>{{ $t('Logged in as {%name%}', { name: $store.state.auth.username }) }}</router-link>
-            <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i> {{ $t('Logout') }}</router-link>
-            <router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> {{ $t('Login') }}</router-link>
+            <router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i>{{ $t('Logout') }}</router-link>
+            <router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i>{{ $t('Login') }}</router-link>
           </div>
         </div>
         <div class="item">
           <div class="header">{{ $t('Music') }}</div>
           <div class="menu">
             <router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>{{ $t('Browse library') }}</router-link>
-            <router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i> {{ $t('Favorites') }}</router-link>
+            <router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i>{{ $t('Favorites') }}</router-link>
             <a
               @click="$store.commit('playlists/chooseTrack', null)"
               v-if="$store.state.auth.authenticated"
               class="item">
-              <i class="list icon"></i> {{ $t('Playlists') }}
+              <i class="list icon"></i>{{ $t('Playlists') }}
             </a>
             <router-link
               v-if="$store.state.auth.authenticated"
-              class="item" :to="{path: '/activity'}"><i class="bell icon"></i> {{ $t('Activity') }}</router-link>
+              class="item" :to="{path: '/activity'}"><i class="bell icon"></i>{{ $t('Activity') }}</router-link>
           </div>
         </div>
         <div class="item" v-if="showAdmin">
@@ -62,7 +62,7 @@
               class="item"
               v-if="$store.state.auth.availablePermissions['import.launch']"
               :to="{name: 'library.requests', query: {status: 'pending' }}">
-              <i class="download icon"></i> {{ $t('Import requests') }}
+              <i class="download icon"></i>{{ $t('Import requests') }}
               <div
                 :class="['ui', {'teal': notifications.importRequests > 0}, 'label']"
                 :title="$t('Pending import requests')">
@@ -72,7 +72,7 @@
               class="item"
               v-if="$store.state.auth.availablePermissions['federation.manage']"
               :to="{path: '/manage/federation/libraries'}">
-              <i class="sitemap icon"></i> {{ $t('Federation') }}
+              <i class="sitemap icon"></i>{{ $t('Federation') }}
               <div
                 :class="['ui', {'teal': notifications.federation > 0}, 'label']"
                 :title="$t('Pending follow requests')">
diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue
index b06ce89f0da850457c10acaee1d894df98368750..f3add57b1cccd736c82eb4f749764a9c0229ab2f 100644
--- a/front/src/components/auth/Login.vue
+++ b/front/src/components/auth/Login.vue
@@ -12,9 +12,15 @@
             </ul>
           </div>
           <div class="field">
-            <i18next tag="label" path="Username or email"/>
+            <label>
+              {{ $t('Username or email') }} |
+              <router-link :to="{path: '/signup'}">
+                {{ $t('Create an account') }}
+              </router-link>
+            </label>
             <input
             ref="username"
+            tabindex="1"
             required
             type="text"
             autofocus
@@ -23,18 +29,16 @@
             >
           </div>
           <div class="field">
-            <i18next tag="label" path="Password"/>            
-            <input
-            required
-            type="password"
-            placeholder="Enter your password"
-            v-model="credentials.password"
-            >
+            <label>
+              {{ $t('Password') }} |
+              <router-link :to="{name: 'auth.password-reset', query: {email: credentials.username}}">
+                {{ $t('Reset your password') }}
+              </router-link>
+            </label>
+            <password-input :index="2" required v-model="credentials.password" />
+
           </div>
-          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Login"/></button>
-          <router-link class="ui right floated basic button" :to="{path: '/signup'}">
-            <i18next path="Create an account"/>
-          </router-link>
+          <button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit"><i18next path="Login"/></button>
         </form>
       </div>
     </div>
@@ -42,12 +46,15 @@
 </template>
 
 <script>
+import PasswordInput from '@/components/forms/PasswordInput'
 
 export default {
-  name: 'login',
   props: {
     next: {type: String, default: '/'}
   },
+  components: {
+    PasswordInput
+  },
   data () {
     return {
       // We need to initialize the component with any
diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue
index c847bde888efb2a0b3bca619dd5cc17e6c3b62d4..8eeae85a94a0831f2008428d92b4712547b0533a 100644
--- a/front/src/components/auth/Settings.vue
+++ b/front/src/components/auth/Settings.vue
@@ -35,21 +35,13 @@
           </div>
           <div class="field">
             <label><i18next path="Old password"/></label>
-            <input
-            required
-            type="password"
-            autofocus
-            placeholder="Enter your old password"
-            v-model="old_password">
+            <password-input required v-model="old_password" />
+
           </div>
           <div class="field">
             <label><i18next path="New password"/></label>
-            <input
-            required
-            type="password"
-            autofocus
-            placeholder="Enter your new password"
-            v-model="new_password">
+            <password-input required v-model="new_password" />
+
           </div>
           <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Change password"/></button>
         </form>
@@ -62,8 +54,12 @@
 import $ from 'jquery'
 import axios from 'axios'
 import logger from '@/logging'
+import PasswordInput from '@/components/forms/PasswordInput'
 
 export default {
+  components: {
+    PasswordInput
+  },
   data () {
     let d = {
       // We need to initialize the component with any
diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue
index 57966264f99f0aa732f5ecd306bc6d875f9ca9fc..89f4cb1f1266c956283142cfc4f470e3b0c5d031 100644
--- a/front/src/components/auth/Signup.vue
+++ b/front/src/components/auth/Signup.vue
@@ -34,16 +34,7 @@
           </div>
           <div class="field">
             <i18next tag="label" path="Password"/>
-            <div class="ui action input">
-              <input
-              required
-              :type="passwordInputType"
-              placeholder="Enter your password"
-              v-model="password">
-              <span @click="showPassword = !showPassword" title="Show/hide password" class="ui icon button">
-                <i class="eye icon"></i>
-              </span>
-            </div>
+            <password-input v-model="password" />
           </div>
           <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"><i18next path="Create my account"/></button>
         </form>
@@ -57,8 +48,13 @@
 import axios from 'axios'
 import logger from '@/logging'
 
+import PasswordInput from '@/components/forms/PasswordInput'
+
 export default {
   name: 'login',
+  components: {
+    PasswordInput
+  },
   props: {
     next: {type: String, default: '/'}
   },
@@ -69,8 +65,7 @@ export default {
       password: '',
       isLoadingInstanceSetting: true,
       errors: [],
-      isLoading: false,
-      showPassword: false
+      isLoading: false
     }
   },
   created () {
@@ -104,16 +99,7 @@ export default {
         self.isLoading = false
       })
     }
-  },
-  computed: {
-    passwordInputType () {
-      if (this.showPassword) {
-        return 'text'
-      }
-      return 'password'
-    }
   }
-
 }
 </script>
 
diff --git a/front/src/components/forms/PasswordInput.vue b/front/src/components/forms/PasswordInput.vue
new file mode 100644
index 0000000000000000000000000000000000000000..624a92d87c8d204ec9b8e40b247e1e7835ed5951
--- /dev/null
+++ b/front/src/components/forms/PasswordInput.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="ui action input">
+    <input
+    required
+    :tabindex="index"
+    :type="passwordInputType"
+    @input="$emit('input', $event.target.value)"
+    :value="value">
+    <span @click="showPassword = !showPassword" :title="$t('Show/hide password')" class="ui icon button">
+      <i class="eye icon"></i>
+    </span>
+  </div>
+</template>
+<script>
+export default {
+  props: ['value', 'index'],
+  data () {
+    return {
+      showPassword: false
+    }
+  },
+  computed: {
+    passwordInputType () {
+      if (this.showPassword) {
+        return 'text'
+      }
+      return 'password'
+    }
+  }
+}
+</script>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 2e06bda99b7869d1af9a823451875fd14a81dcf1..b1e208023335945f42c7b255aa05ebaffeac5e2b 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -9,6 +9,9 @@ import Signup from '@/components/auth/Signup'
 import Profile from '@/components/auth/Profile'
 import Settings from '@/components/auth/Settings'
 import Logout from '@/components/auth/Logout'
+import PasswordReset from '@/views/auth/PasswordReset'
+import PasswordResetConfirm from '@/views/auth/PasswordResetConfirm'
+import EmailConfirm from '@/views/auth/EmailConfirm'
 import Library from '@/components/library/Library'
 import LibraryHome from '@/components/library/Home'
 import LibraryArtist from '@/components/library/Artist'
@@ -59,6 +62,31 @@ export default new Router({
       component: Login,
       props: (route) => ({ next: route.query.next || '/library' })
     },
+    {
+      path: '/auth/password/reset',
+      name: 'auth.password-reset',
+      component: PasswordReset,
+      props: (route) => ({
+        defaultEmail: route.query.email
+      })
+    },
+    {
+      path: '/auth/email/confirm',
+      name: 'auth.email-confirm',
+      component: EmailConfirm,
+      props: (route) => ({
+        defaultKey: route.query.key
+      })
+    },
+    {
+      path: '/auth/password/reset/confirm',
+      name: 'auth.password-reset-confirm',
+      component: PasswordResetConfirm,
+      props: (route) => ({
+        defaultUid: route.query.uid,
+        defaultToken: route.query.token
+      })
+    },
     {
       path: '/signup',
       name: 'signup',
diff --git a/front/src/store/auth.js b/front/src/store/auth.js
index b1753404f9be65c2d5fe2a067607d83ef45d4d6a..68a15090b5c289d2825563743f4cad7f2d3cdbf0 100644
--- a/front/src/store/auth.js
+++ b/front/src/store/auth.js
@@ -97,6 +97,11 @@ export default {
       }
     },
     fetchProfile ({commit, dispatch, state}) {
+      if (document) {
+        // this is to ensure we do not have any leaking cookie set by django
+        document.cookie = 'sessionid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
+      }
+
       return axios.get('users/users/me/').then((response) => {
         logger.default.info('Successfully fetched user profile')
         let data = response.data
diff --git a/front/src/store/player.js b/front/src/store/player.js
index ed437c3f0220d84ed26693c94a8036c38c756b64..2149b51ffc63587a60df9eaa655d6652deda964f 100644
--- a/front/src/store/player.js
+++ b/front/src/store/player.js
@@ -85,7 +85,10 @@ export default {
     togglePlay ({commit, state}) {
       commit('playing', !state.playing)
     },
-    trackListened ({commit}, track) {
+    trackListened ({commit, rootState}, track) {
+      if (!rootState.auth.authenticated) {
+        return
+      }
       return axios.post('history/listenings/', {'track': track.id}).then((response) => {}, (response) => {
         logger.default.error('Could not record track in history')
       })
diff --git a/front/src/views/auth/EmailConfirm.vue b/front/src/views/auth/EmailConfirm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7ffa3c8d1bb96073f34985000bd624a22fe0523e
--- /dev/null
+++ b/front/src/views/auth/EmailConfirm.vue
@@ -0,0 +1,71 @@
+<template>
+  <div class="main pusher" v-title="$t('Confirm your email')">
+    <div class="ui vertical stripe segment">
+      <div class="ui small text container">
+        <h2>{{ $t('Confirm your email') }}</h2>
+        <form v-if="!success" class="ui form" @submit.prevent="submit()">
+          <div v-if="errors.length > 0" class="ui negative message">
+            <div class="header">{{ $t('Error while confirming your email') }}</div>
+            <ul class="list">
+              <li v-for="error in errors">{{ error }}</li>
+            </ul>
+          </div>
+          <div class="field">
+            <label>{{ $t('Confirmation code') }}</label>
+            <input type="text" required v-model="key" />
+          </div>
+          <router-link :to="{path: '/login'}">
+            {{ $t('Back to login') }}
+          </router-link>
+          <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
+            {{ $t('Confirm your email') }}</button>
+        </form>
+        <div v-else class="ui positive message">
+          <div class="header">{{ $t('Email confirmed') }}</div>
+          <p>{{ $t('Your email address was confirmed, you can now use the service without limitations.') }}</p>
+          <router-link :to="{name: 'login'}">
+            {{ $t('Proceed to login') }}
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  props: ['defaultKey'],
+  data () {
+    return {
+      isLoading: false,
+      errors: [],
+      key: this.defaultKey,
+      success: false
+    }
+  },
+  methods: {
+    submit () {
+      let self = this
+      self.isLoading = true
+      self.errors = []
+      let payload = {
+        key: this.key
+      }
+      return axios.post('auth/registration/verify-email/', payload).then(response => {
+        self.isLoading = false
+        self.success = true
+      }, error => {
+        self.errors = error.backendErrors
+        self.isLoading = false
+      })
+    }
+  }
+
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/auth/PasswordReset.vue b/front/src/views/auth/PasswordReset.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f6b445e00fa4e5f38e852b0f5c9c50c407ffd820
--- /dev/null
+++ b/front/src/views/auth/PasswordReset.vue
@@ -0,0 +1,75 @@
+<template>
+  <div class="main pusher" v-title="$t('Reset your password')">
+    <div class="ui vertical stripe segment">
+      <div class="ui small text container">
+        <h2>{{ $t('Reset your password') }}</h2>
+        <form class="ui form" @submit.prevent="submit()">
+          <div v-if="errors.length > 0" class="ui negative message">
+            <div class="header">{{ $t('Error while asking for a password reset') }}</div>
+            <ul class="list">
+              <li v-for="error in errors">{{ error }}</li>
+            </ul>
+          </div>
+          <p>{{ $t('Use this form to request a password reset. We will send an email to the given address with instructions to reset your password.') }}</p>
+          <div class="field">
+            <label>{{ $t('Account\'s email') }}</label>
+            <input
+              required
+              ref="email"
+              type="email"
+              autofocus
+              :placeholder="$t('Input the email address binded to your account')"
+              v-model="email">
+          </div>
+          <router-link :to="{path: '/login'}">
+            {{ $t('Back to login') }}
+          </router-link>
+          <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
+            {{ $t('Ask for a password reset') }}</button>
+        </form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  props: ['defaultEmail'],
+  data () {
+    return {
+      email: this.defaultEmail,
+      isLoading: false,
+      errors: []
+    }
+  },
+  mounted () {
+    this.$refs.email.focus()
+  },
+  methods: {
+    submit () {
+      let self = this
+      self.isLoading = true
+      self.errors = []
+      let payload = {
+        email: this.email
+      }
+      return axios.post('auth/password/reset/', payload).then(response => {
+        self.isLoading = false
+        self.$router.push({
+          name: 'auth.password-reset-confirm'
+        })
+      }, error => {
+        self.errors = error.backendErrors
+        self.isLoading = false
+      })
+    }
+  }
+
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/auth/PasswordResetConfirm.vue b/front/src/views/auth/PasswordResetConfirm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..102ed6126d1275cc4cbfd9789dff54a0ba40784c
--- /dev/null
+++ b/front/src/views/auth/PasswordResetConfirm.vue
@@ -0,0 +1,85 @@
+<template>
+  <div class="main pusher" v-title="$t('Change your password')">
+    <div class="ui vertical stripe segment">
+      <div class="ui small text container">
+        <h2>{{ $t('Change your password') }}</h2>
+        <form v-if="!success" class="ui form" @submit.prevent="submit()">
+          <div v-if="errors.length > 0" class="ui negative message">
+            <div class="header">{{ $t('Error while changing your password') }}</div>
+            <ul class="list">
+              <li v-for="error in errors">{{ error }}</li>
+            </ul>
+          </div>
+          <template v-if="token && uid">
+            <div class="field">
+              <label>{{ $t('New password') }}</label>
+              <password-input v-model="newPassword" />
+            </div>
+            <router-link :to="{path: '/login'}">
+              {{ $t('Back to login') }}
+            </router-link>
+            <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
+              {{ $t('Update your password') }}</button>
+          </template>
+          <template v-else>
+            <p>{{ $t('If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes.') }}</p>
+          </template>
+        </form>
+        <div v-else class="ui positive message">
+          <div class="header">{{ $t('Password updated successfully') }}</div>
+          <p>{{ $t('Your password has been updated successfully.') }}</p>
+          <router-link :to="{name: 'login'}">
+            {{ $t('Proceed to login') }}
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import PasswordInput from '@/components/forms/PasswordInput'
+
+export default {
+  props: ['defaultToken', 'defaultUid'],
+  components: {
+    PasswordInput
+  },
+  data () {
+    return {
+      newPassword: '',
+      isLoading: false,
+      errors: [],
+      token: this.defaultToken,
+      uid: this.defaultUid,
+      success: false
+    }
+  },
+  methods: {
+    submit () {
+      let self = this
+      self.isLoading = true
+      self.errors = []
+      let payload = {
+        uid: this.uid,
+        token: this.token,
+        new_password1: this.newPassword,
+        new_password2: this.newPassword
+      }
+      return axios.post('auth/password/reset/confirm/', payload).then(response => {
+        self.isLoading = false
+        self.success = true
+      }, error => {
+        self.errors = error.backendErrors
+        self.isLoading = false
+      })
+    }
+  }
+
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>