Repository: pypi/warehouse

Issue:
list recent sent emails in security/account settings
**What's the problem this feature will solve?**
Sometimes administrators want to send our users email notifications to ask that they do something, but we don't want to inure them to clicking on links in emails, and we want a way to credibly demonstrate that the email is legitimate.

**Describe the solution you'd like**
Via <a href="https://twitter.com/runasand/status/1248385736677576706">this tweet</a> I saw that Facebook archives recent automated emails sent to users so that each user can check in their settings to find those notifications:

> To confirm whether an email claiming to be from Facebook is authentic, review recent emails we've sent in your Security and Login Settings

Could we do this?

**Additional context**
In the next 12-18 months, PyPI will probably add more features or increase the strictness of authentication, etc. for project maintainers and owners. When we do those things, we'll want to notify users in a credible way.


Discussion:
> Could we do this?

Seems reasonable. We could potentially add sent emails to the existing "Security History" feed.

Does the Facebook implementation of this feature include the entire email, or just the subject/timestamp?
Good question! I don't know. (I don't have a Facebook account.)
Me neither 🙂 
It's a table, with:

- Date (eg: "July 20, 20:26")
- Sent to (i.e. the email)
- Email subject

---

I've made a lot of questionable choices in life. :)

Resolving patch:
diff --git a/warehouse/admin/views/emails.py b/warehouse/admin/views/emails.py
--- a/warehouse/admin/views/emails.py
+++ b/warehouse/admin/views/emails.py
@@ -99,6 +99,17 @@ def email_mass(request):
                         "body_text": row["body_text"],
                         "body_html": row.get("body_html"),
                     },
+                    {
+                        "tag": "account:email:sent",
+                        "user_id": user.id,
+                        "ip_address": request.remote_addr,
+                        "additional": {
+                            "from_": request.registry.settings.get("mail.sender"),
+                            "to": email.email,
+                            "subject": row["subject"],
+                            "redact_ip": True,
+                        },
+                    },
                 )
         request.session.flash("Mass emails sent", queue="success")
     else:
diff --git a/warehouse/email/__init__.py b/warehouse/email/__init__.py
--- a/warehouse/email/__init__.py
+++ b/warehouse/email/__init__.py
@@ -20,7 +20,8 @@
 from first import first
 
 from warehouse import tasks
-from warehouse.accounts.interfaces import ITokenService
+from warehouse.accounts.interfaces import ITokenService, IUserService
+from warehouse.accounts.models import Email
 from warehouse.email.interfaces import IEmailSender
 from warehouse.email.services import EmailMessage
 from warehouse.email.ses.tasks import cleanup as ses_cleanup
@@ -33,12 +34,15 @@ def _compute_recipient(user, email):
 
 
 @tasks.task(bind=True, ignore_result=True, acks_late=True)
-def send_email(task, request, recipient, msg):
+def send_email(task, request, recipient, msg, success_event):
     msg = EmailMessage(**msg)
     sender = request.find_service(IEmailSender)
 
     try:
         sender.send(recipient, msg)
+
+        user_service = request.find_service(IUserService, context=None)
+        user_service.record_event(**success_event)
     except Exception as exc:
         task.retry(exc=exc)
 
@@ -56,8 +60,27 @@ def _send_email_to_user(request, user, msg, *, email=None, allow_unverified=Fals
     if email is None or not (email.verified or allow_unverified):
         return
 
+    # We should only store/display IP address of an 'email sent' event if the user
+    # who triggered the email event is the one who receives the email. Else display
+    # 'Redacted' to prevent user privacy concerns. If we don't know the user who
+    # triggered the action, default to showing the IP of the source.
+    user_email = request.db.query(Email).filter(Email.email == email.email).one()
+    redact_ip = user_email.user_id != request.user.id if request.user else False
+
     request.task(send_email).delay(
-        _compute_recipient(user, email.email), attr.asdict(msg)
+        _compute_recipient(user, email.email),
+        attr.asdict(msg),
+        {
+            "tag": "account:email:sent",
+            "user_id": user.id,
+            "ip_address": request.remote_addr,
+            "additional": {
+                "from_": request.registry.settings.get("mail.sender"),
+                "to": email.email,
+                "subject": msg.subject,
+                "redact_ip": redact_ip,
+            },
+        },
     )
 
 


Test patch:
diff --git a/tests/unit/admin/views/test_emails.py b/tests/unit/admin/views/test_emails.py
--- a/tests/unit/admin/views/test_emails.py
+++ b/tests/unit/admin/views/test_emails.py
@@ -145,9 +145,11 @@ def test_sends_emails(self, db_request):
         db_request.params = {"csvfile": pretend.stub(file=input_file)}
         db_request.task = lambda a: pretend.stub(delay=delay)
         db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+        db_request.remote_addr = "0.0.0.0"
         db_request.session = pretend.stub(
             flash=pretend.call_recorder(lambda *a, **kw: None)
         )
+        db_request.registry.settings = {"mail.sender": "noreply@example.com"}
 
         result = views.email_mass(db_request)
 
@@ -165,6 +167,17 @@ def test_sends_emails(self, db_request):
                     "body_text": "Test Body 1",
                     "body_html": None,
                 },
+                {
+                    "tag": "account:email:sent",
+                    "user_id": user1.id,
+                    "ip_address": db_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": email1.email,
+                        "subject": "Test Subject 1",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 email2.email,
@@ -173,6 +186,17 @@ def test_sends_emails(self, db_request):
                     "body_text": "Test Body 2",
                     "body_html": None,
                 },
+                {
+                    "tag": "account:email:sent",
+                    "user_id": user2.id,
+                    "ip_address": db_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": email2.email,
+                        "subject": "Test Subject 2",
+                        "redact_ip": True,
+                    },
+                },
             ),
         ]
 
diff --git a/tests/unit/email/test_init.py b/tests/unit/email/test_init.py
--- a/tests/unit/email/test_init.py
+++ b/tests/unit/email/test_init.py
@@ -18,7 +18,7 @@
 import pytest
 
 from warehouse import email
-from warehouse.accounts.interfaces import ITokenService
+from warehouse.accounts.interfaces import ITokenService, IUserService
 from warehouse.email.interfaces import IEmailSender
 from warehouse.email.services import EmailMessage
 
@@ -82,13 +82,26 @@ class TestSendEmailToUser:
     def test_sends_to_user_with_verified(
         self, name, username, primary_email, address, expected
     ):
-        task = pretend.stub(delay=pretend.call_recorder(lambda *a, **kw: None))
-        request = pretend.stub(task=pretend.call_recorder(lambda x: task))
-
         user = pretend.stub(
             name=name,
             username=username,
             primary_email=pretend.stub(email=primary_email, verified=True),
+            id="id",
+        )
+
+        task = pretend.stub(delay=pretend.call_recorder(lambda *a, **kw: None))
+        request = pretend.stub(
+            task=pretend.call_recorder(lambda x: task),
+            db=pretend.stub(
+                query=lambda a: pretend.stub(
+                    filter=lambda *a: pretend.stub(
+                        one=lambda: pretend.stub(user_id=user.id)
+                    )
+                ),
+            ),
+            remote_addr="0.0.0.0",
+            user=user,
+            registry=pretend.stub(settings={"mail.sender": "noreply@example.com"}),
         )
 
         if address is not None:
@@ -103,6 +116,17 @@ def test_sends_to_user_with_verified(
             pretend.call(
                 expected,
                 {"subject": "My Subject", "body_text": "My Body", "body_html": None},
+                {
+                    "tag": "account:email:sent",
+                    "user_id": user.id,
+                    "ip_address": request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": address.email if address else primary_email,
+                        "subject": "My Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -120,7 +144,7 @@ def test_doesnt_send_with_unverified(self, primary_email, address):
         user = pretend.stub(
             primary_email=pretend.stub(
                 email=primary_email, verified=True if address is not None else False
-            )
+            ),
         )
 
         if address is not None:
@@ -148,15 +172,28 @@ def test_doesnt_send_with_unverified(self, primary_email, address):
     def test_sends_unverified_with_override(
         self, username, primary_email, address, expected
     ):
-        task = pretend.stub(delay=pretend.call_recorder(lambda *a, **kw: None))
-        request = pretend.stub(task=pretend.call_recorder(lambda x: task))
-
         user = pretend.stub(
             username=username,
             name="",
             primary_email=pretend.stub(
                 email=primary_email, verified=True if address is not None else False
             ),
+            id="id",
+        )
+
+        task = pretend.stub(delay=pretend.call_recorder(lambda *a, **kw: None))
+        request = pretend.stub(
+            task=pretend.call_recorder(lambda x: task),
+            db=pretend.stub(
+                query=lambda a: pretend.stub(
+                    filter=lambda *a: pretend.stub(
+                        one=lambda: pretend.stub(user_id=user.id)
+                    )
+                ),
+            ),
+            remote_addr="0.0.0.0",
+            user=user,
+            registry=pretend.stub(settings={"mail.sender": "noreply@example.com"}),
         )
 
         if address is not None:
@@ -173,12 +210,23 @@ def test_sends_unverified_with_override(
             pretend.call(
                 expected,
                 {"subject": "My Subject", "body_text": "My Body", "body_html": None},
+                {
+                    "tag": "account:email:sent",
+                    "user_id": user.id,
+                    "ip_address": request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": address.email if address else primary_email,
+                        "subject": "My Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
 
 class TestSendEmail:
-    def test_send_email_success(self, monkeypatch):
+    def test_send_email_success(self, db_session, monkeypatch):
         class FakeMailSender:
             def __init__(self):
                 self.emails = []
@@ -193,17 +241,57 @@ def send(self, recipient, msg):
                     }
                 )
 
+        class FakeUserEventService:
+            def __init__(self):
+                self.events = []
+
+            def record_event(self, user_id, tag, ip_address, additional):
+                self.events.append(
+                    {
+                        "user_id": user_id,
+                        "tag": tag,
+                        "ip_address": ip_address,
+                        "additional": additional,
+                    }
+                )
+
+        user_service = FakeUserEventService()
         sender = FakeMailSender()
         task = pretend.stub()
         request = pretend.stub(
-            find_service=pretend.call_recorder(lambda *a, **kw: sender)
+            find_service=pretend.call_recorder(
+                lambda svc, context=None: {
+                    IUserService: user_service,
+                    IEmailSender: sender,
+                }.get(svc)
+            ),
         )
+        user_id = pretend.stub()
 
         msg = EmailMessage(subject="subject", body_text="body")
 
-        email.send_email(task, request, "recipient", attr.asdict(msg))
-
-        assert request.find_service.calls == [pretend.call(IEmailSender)]
+        email.send_email(
+            task,
+            request,
+            "recipient",
+            attr.asdict(msg),
+            {
+                "tag": "account:email:sent",
+                "user_id": user_id,
+                "ip_address": "0.0.0.0",
+                "additional": {
+                    "from_": "noreply@example.com",
+                    "to": "recipient",
+                    "subject": msg.subject,
+                    "redact_ip": False,
+                },
+            },
+        )
+
+        assert request.find_service.calls == [
+            pretend.call(IEmailSender),
+            pretend.call(IUserService, context=None),
+        ]
         assert sender.emails == [
             {
                 "subject": "subject",
@@ -212,6 +300,19 @@ def send(self, recipient, msg):
                 "recipient": "recipient",
             }
         ]
+        assert user_service.events == [
+            {
+                "user_id": user_id,
+                "tag": "account:email:sent",
+                "ip_address": "0.0.0.0",
+                "additional": {
+                    "from_": "noreply@example.com",
+                    "to": "recipient",
+                    "subject": msg.subject,
+                    "redact_ip": False,
+                },
+            }
+        ]
 
     def test_send_email_failure(self, monkeypatch):
         exc = Exception()
@@ -228,10 +329,27 @@ def retry(exc):
 
         sender, task = FakeMailSender(), Task()
         request = pretend.stub(find_service=lambda *a, **kw: sender)
+        user_id = pretend.stub()
         msg = EmailMessage(subject="subject", body_text="body")
 
         with pytest.raises(celery.exceptions.Retry):
-            email.send_email(task, request, "recipient", attr.asdict(msg))
+            email.send_email(
+                task,
+                request,
+                "recipient",
+                attr.asdict(msg),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": user_id,
+                    "ip_address": "0.0.0.0",
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "recipient",
+                        "subject": msg.subject,
+                        "redact_ip": False,
+                    },
+                },
+            )
 
         assert task.retry.calls == [pretend.call(exc=exc)]
 
@@ -294,6 +412,17 @@ def test_send_password_reset_email(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_password_reset_email(
             pyramid_request, (stub_user, stub_email)
         )
@@ -335,6 +464,19 @@ def test_send_password_reset_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "other@example.com"
+                        if stub_email
+                        else "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -344,6 +486,9 @@ def test_email_verification_email(
         self, pyramid_request, pyramid_config, token_service, monkeypatch
     ):
 
+        stub_user = pretend.stub(
+            id="id", username=None, name=None, email="foo@example.com",
+        )
         stub_email = pretend.stub(id="id", email="email@example.com", verified=False)
         pyramid_request.method = "POST"
         token_service.dumps = pretend.call_recorder(lambda a: "TOKEN")
@@ -370,13 +515,20 @@ def test_email_verification_email(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
-        result = email.send_email_verification_email(
-            pyramid_request,
-            (
-                pretend.stub(username=None, name=None, email="foo@example.com"),
-                stub_email,
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
             ),
         )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
+        result = email.send_email_verification_email(
+            pyramid_request, (stub_user, stub_email,),
+        )
 
         assert result == {
             "token": "TOKEN",
@@ -406,6 +558,17 @@ def test_email_verification_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": stub_email.email,
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -413,6 +576,7 @@ def test_email_verification_email(
 class TestPasswordChangeEmail:
     def test_password_change_email(self, pyramid_request, pyramid_config, monkeypatch):
         stub_user = pretend.stub(
+            id="id",
             username="username",
             name="",
             email="email@example.com",
@@ -437,6 +601,17 @@ def test_password_change_email(self, pyramid_request, pyramid_config, monkeypatc
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_password_change_email(pyramid_request, stub_user)
 
         assert result == {"username": stub_user.username}
@@ -457,6 +632,17 @@ def test_password_change_email(self, pyramid_request, pyramid_config, monkeypatc
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": stub_user.email,
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -488,6 +674,17 @@ def test_password_change_email_unverified(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_password_change_email(pyramid_request, stub_user)
 
         assert result == {"username": stub_user.username}
@@ -504,6 +701,7 @@ def test_password_compromised_email_hibp(
         self, pyramid_request, pyramid_config, monkeypatch, verified
     ):
         stub_user = pretend.stub(
+            id="id",
             username="username",
             name="",
             email="email@example.com",
@@ -528,6 +726,17 @@ def test_password_compromised_email_hibp(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_password_compromised_email_hibp(pyramid_request, stub_user)
 
         assert result == {}
@@ -545,6 +754,17 @@ def test_password_compromised_email_hibp(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": stub_user.email,
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -555,6 +775,7 @@ def test_password_compromised_email(
         self, pyramid_request, pyramid_config, monkeypatch, verified
     ):
         stub_user = pretend.stub(
+            id="id",
             username="username",
             name="",
             email="email@example.com",
@@ -579,6 +800,17 @@ def test_password_compromised_email(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_password_compromised_email(pyramid_request, stub_user)
 
         assert result == {}
@@ -596,6 +828,17 @@ def test_password_compromised_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": stub_user.email,
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -604,6 +847,7 @@ class TestAccountDeletionEmail:
     def test_account_deletion_email(self, pyramid_request, pyramid_config, monkeypatch):
 
         stub_user = pretend.stub(
+            id="id",
             username="username",
             name="",
             email="email@example.com",
@@ -628,6 +872,17 @@ def test_account_deletion_email(self, pyramid_request, pyramid_config, monkeypat
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_account_deletion_email(pyramid_request, stub_user)
 
         assert result == {"username": stub_user.username}
@@ -648,6 +903,17 @@ def test_account_deletion_email(self, pyramid_request, pyramid_config, monkeypat
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": stub_user.email,
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -656,6 +922,7 @@ def test_account_deletion_email_unverified(
     ):
 
         stub_user = pretend.stub(
+            id="id",
             username="username",
             name="",
             email="email@example.com",
@@ -680,6 +947,17 @@ def test_account_deletion_email_unverified(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_account_deletion_email(pyramid_request, stub_user)
 
         assert result == {"username": stub_user.username}
@@ -696,7 +974,7 @@ def test_primary_email_change_email(
     ):
 
         stub_user = pretend.stub(
-            email="new_email@example.com", username="username", name=""
+            id="id", email="new_email@example.com", username="username", name=""
         )
         subject_renderer = pyramid_config.testing_add_renderer(
             "email/primary-email-change/subject.txt"
@@ -717,6 +995,17 @@ def test_primary_email_change_email(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_primary_email_change_email(
             pyramid_request,
             (stub_user, pretend.stub(email="old_email@example.com", verified=True)),
@@ -744,6 +1033,17 @@ def test_primary_email_change_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "old_email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -752,7 +1052,7 @@ def test_primary_email_change_email_unverified(
     ):
 
         stub_user = pretend.stub(
-            email="new_email@example.com", username="username", name=""
+            id="id", email="new_email@example.com", username="username", name=""
         )
         subject_renderer = pyramid_config.testing_add_renderer(
             "email/primary-email-change/subject.txt"
@@ -773,6 +1073,17 @@ def test_primary_email_change_email_unverified(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_primary_email_change_email(
             pyramid_request,
             (stub_user, pretend.stub(email="old_email@example.com", verified=False)),
@@ -796,12 +1107,14 @@ def test_collaborator_added_email(
     ):
 
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -828,6 +1141,18 @@ def test_collaborator_added_email(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_collaborator_added_email(
             pyramid_request,
             [stub_user, stub_submitter_user],
@@ -870,6 +1195,17 @@ def test_collaborator_added_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -883,6 +1219,17 @@ def test_collaborator_added_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -891,12 +1238,14 @@ def test_collaborator_added_email_unverified(
     ):
 
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=False),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -923,6 +1272,17 @@ def test_collaborator_added_email_unverified(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_submitter_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_collaborator_added_email(
             pyramid_request,
             [stub_user, stub_submitter_user],
@@ -962,6 +1322,17 @@ def test_collaborator_added_email_unverified(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]
 
@@ -972,13 +1343,14 @@ def test_added_as_collaborator_email(
     ):
 
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
-            username="submitterusername", email="submiteremail"
+            id="id_2", username="submitterusername", email="submiteremail",
         )
         subject_renderer = pyramid_config.testing_add_renderer(
             "email/added-as-collaborator/subject.txt"
@@ -999,6 +1371,17 @@ def test_added_as_collaborator_email(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_added_as_collaborator_email(
             pyramid_request,
             stub_user,
@@ -1034,6 +1417,17 @@ def test_added_as_collaborator_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             )
         ]
 
@@ -1042,13 +1436,14 @@ def test_added_as_collaborator_email_unverified(
     ):
 
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=False),
         )
         stub_submitter_user = pretend.stub(
-            username="submitterusername", email="submiteremail"
+            id="id_2", username="submitterusername", email="submiteremail",
         )
         subject_renderer = pyramid_config.testing_add_renderer(
             "email/added-as-collaborator/subject.txt"
@@ -1069,6 +1464,17 @@ def test_added_as_collaborator_email_unverified(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_added_as_collaborator_email(
             pyramid_request,
             stub_user,
@@ -1099,12 +1505,14 @@ def test_removed_project_email_to_maintainer(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1131,6 +1539,18 @@ def test_removed_project_email_to_maintainer(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_removed_project_email(
             pyramid_request,
             [stub_user, stub_submitter_user],
@@ -1171,6 +1591,17 @@ def test_removed_project_email_to_maintainer(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1184,6 +1615,17 @@ def test_removed_project_email_to_maintainer(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1191,12 +1633,14 @@ def test_removed_project_email_to_owner(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1223,6 +1667,18 @@ def test_removed_project_email_to_owner(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         result = email.send_removed_project_email(
             pyramid_request,
             [stub_user, stub_submitter_user],
@@ -1263,6 +1719,17 @@ def test_removed_project_email_to_owner(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1276,6 +1743,17 @@ def test_removed_project_email_to_owner(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1285,12 +1763,14 @@ def test_send_yanked_project_release_email_to_maintainer(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1318,6 +1798,18 @@ def test_send_yanked_project_release_email_to_maintainer(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -1371,6 +1863,17 @@ def test_send_yanked_project_release_email_to_maintainer(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1384,6 +1887,17 @@ def test_send_yanked_project_release_email_to_maintainer(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1391,12 +1905,14 @@ def test_send_yanked_project_release_email_to_owner(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1424,6 +1940,18 @@ def test_send_yanked_project_release_email_to_owner(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -1477,6 +2005,17 @@ def test_send_yanked_project_release_email_to_owner(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1490,6 +2029,17 @@ def test_send_yanked_project_release_email_to_owner(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1499,12 +2049,14 @@ def test_send_unyanked_project_release_email_to_maintainer(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1532,6 +2084,18 @@ def test_send_unyanked_project_release_email_to_maintainer(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -1584,6 +2148,17 @@ def test_send_unyanked_project_release_email_to_maintainer(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1597,6 +2172,17 @@ def test_send_unyanked_project_release_email_to_maintainer(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1604,12 +2190,14 @@ def test_send_unyanked_project_release_email_to_owner(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1637,6 +2225,18 @@ def test_send_unyanked_project_release_email_to_owner(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -1689,6 +2289,17 @@ def test_send_unyanked_project_release_email_to_owner(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1702,6 +2313,17 @@ def test_send_unyanked_project_release_email_to_owner(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1711,12 +2333,14 @@ def test_send_removed_project_release_email_to_maintainer(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1744,6 +2368,18 @@ def test_send_removed_project_release_email_to_maintainer(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -1796,6 +2432,17 @@ def test_send_removed_project_release_email_to_maintainer(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1809,6 +2456,17 @@ def test_send_removed_project_release_email_to_maintainer(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1816,12 +2474,14 @@ def test_send_removed_project_release_email_to_owner(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1849,6 +2509,18 @@ def test_send_removed_project_release_email_to_owner(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -1901,6 +2573,17 @@ def test_send_removed_project_release_email_to_owner(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -1914,6 +2597,17 @@ def test_send_removed_project_release_email_to_owner(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -1923,12 +2617,14 @@ def test_send_removed_project_release_file_email_to_owner(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -1956,6 +2652,18 @@ def test_send_removed_project_release_file_email_to_owner(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -2009,6 +2717,17 @@ def test_send_removed_project_release_file_email_to_owner(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -2022,6 +2741,17 @@ def test_send_removed_project_release_file_email_to_owner(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -2029,12 +2759,14 @@ def test_send_removed_project_release_file_email_to_maintainer(
         self, pyramid_request, pyramid_config, monkeypatch
     ):
         stub_user = pretend.stub(
+            id="id_1",
             username="username",
             name="",
             email="email@example.com",
             primary_email=pretend.stub(email="email@example.com", verified=True),
         )
         stub_submitter_user = pretend.stub(
+            id="id_2",
             username="submitterusername",
             name="",
             email="submiteremail@example.com",
@@ -2062,6 +2794,18 @@ def test_send_removed_project_release_file_email_to_maintainer(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        ids = [stub_submitter_user.id, stub_user.id]
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=ids.pop())
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_submitter_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         release = pretend.stub(
             version="0.0.0",
             project=pretend.stub(name="test_project"),
@@ -2115,6 +2859,17 @@ def test_send_removed_project_release_file_email_to_maintainer(
                         ),
                     ),
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "email@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": True,
+                    },
+                },
             ),
             pretend.call(
                 "submitterusername <submiteremail@example.com>",
@@ -2128,6 +2883,17 @@ def test_send_removed_project_release_file_email_to_maintainer(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_submitter_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": "submiteremail@example.com",
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             ),
         ]
 
@@ -2152,6 +2918,7 @@ def test_two_factor_email(
         pretty_method,
     ):
         stub_user = pretend.stub(
+            id="id",
             username="username",
             name="",
             email="email@example.com",
@@ -2176,6 +2943,17 @@ def test_two_factor_email(
         pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
         monkeypatch.setattr(email, "send_email", send_email)
 
+        pyramid_request.db = pretend.stub(
+            query=lambda a: pretend.stub(
+                filter=lambda *a: pretend.stub(
+                    one=lambda: pretend.stub(user_id=stub_user.id)
+                )
+            ),
+        )
+        pyramid_request.remote_addr = "0.0.0.0"
+        pyramid_request.user = stub_user
+        pyramid_request.registry.settings = {"mail.sender": "noreply@example.com"}
+
         send_method = getattr(email, f"send_two_factor_{action}_email")
         result = send_method(pyramid_request, stub_user, method=method)
 
@@ -2197,5 +2975,16 @@ def test_two_factor_email(
                         ),
                     )
                 ),
+                {
+                    "tag": "account:email:sent",
+                    "user_id": stub_user.id,
+                    "ip_address": pyramid_request.remote_addr,
+                    "additional": {
+                        "from_": "noreply@example.com",
+                        "to": stub_user.email,
+                        "subject": "Email Subject",
+                        "redact_ip": False,
+                    },
+                },
             )
         ]