Skip to content

Commit c8b9bfd

Browse files
dstufftnitinprakash96
authored andcommitted
Move email handling into a service (pypi#3493)
1 parent 6eac4c9 commit c8b9bfd

File tree

8 files changed

+201
-80
lines changed

8 files changed

+201
-80
lines changed

tests/unit/email/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.

tests/unit/test_email.py renamed to tests/unit/email/test_init.py

Lines changed: 43 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,57 +14,50 @@
1414
import pretend
1515
import pytest
1616

17-
from pyramid_mailer.message import Message
18-
from pyramid_mailer.interfaces import IMailer
19-
2017
from warehouse import email
2118
from warehouse.accounts.interfaces import ITokenService
19+
from warehouse.email.interfaces import IEmailSender
2220

2321

2422
class TestSendEmail:
2523

2624
def test_send_email_success(self, monkeypatch):
27-
message_obj = Message()
2825

29-
def mock_message(*args, **kwargs):
30-
return message_obj
26+
class FakeMailSender:
27+
28+
def __init__(self):
29+
self.emails = []
3130

32-
monkeypatch.setattr(email, "Message", mock_message)
31+
def send(self, subject, body, *, recipient):
32+
self.emails.append(
33+
{"subject": subject, "body": body, "recipient": recipient},
34+
)
3335

36+
sender = FakeMailSender()
3437
task = pretend.stub()
35-
mailer = pretend.stub(
36-
send_immediately=pretend.call_recorder(lambda i: None)
37-
)
3838
request = pretend.stub(
39-
registry=pretend.stub(
40-
settings=pretend.stub(
41-
get=pretend.call_recorder(lambda k: 'SENDER'),
42-
),
43-
getUtility=pretend.call_recorder(lambda mailr: mailer)
44-
)
39+
find_service=pretend.call_recorder(lambda *a, **kw: sender),
4540
)
4641

4742
email.send_email(
4843
task,
4944
request,
50-
"body",
5145
"subject",
52-
recipients=["recipients"],
46+
"body",
47+
recipient="recipient",
5348
)
5449

55-
assert mailer.send_immediately.calls == [pretend.call(message_obj)]
56-
assert request.registry.getUtility.calls == [pretend.call(IMailer)]
57-
assert request.registry.settings.get.calls == [
58-
pretend.call("mail.sender")]
50+
assert request.find_service.calls == [pretend.call(IEmailSender)]
51+
assert sender.emails == [
52+
{"subject": "subject", "body": "body", "recipient": "recipient"},
53+
]
5954

6055
def test_send_email_failure(self, monkeypatch):
6156
exc = Exception()
62-
message_obj = Message()
6357

64-
class Mailer:
65-
@staticmethod
66-
@pretend.call_recorder
67-
def send_immediately(message):
58+
class FakeMailSender:
59+
60+
def send(self, subject, body, *, recipient):
6861
raise exc
6962

7063
class Task:
@@ -73,34 +66,18 @@ class Task:
7366
def retry(exc):
7467
raise celery.exceptions.Retry
7568

76-
def mock_message(*args, **kwargs):
77-
return message_obj
78-
79-
monkeypatch.setattr(email, "Message", mock_message)
80-
81-
mailer, task = Mailer(), Task()
82-
request = pretend.stub(
83-
registry=pretend.stub(
84-
settings=pretend.stub(
85-
get=pretend.call_recorder(lambda k: 'SENDER'),
86-
),
87-
getUtility=pretend.call_recorder(lambda mailr: mailer)
88-
)
89-
)
69+
sender, task = FakeMailSender(), Task()
70+
request = pretend.stub(find_service=lambda *a, **kw: sender)
9071

9172
with pytest.raises(celery.exceptions.Retry):
9273
email.send_email(
9374
task,
9475
request,
95-
"body",
9676
"subject",
97-
recipients=["recipients"],
77+
"body",
78+
recipient="recipient",
9879
)
9980

100-
assert mailer.send_immediately.calls == [pretend.call(message_obj)]
101-
assert request.registry.getUtility.calls == [pretend.call(IMailer)]
102-
assert request.registry.settings.get.calls == [
103-
pretend.call("mail.sender")]
10481
assert task.retry.calls == [pretend.call(exc=exc)]
10582

10683

@@ -167,9 +144,9 @@ def test_send_password_reset_email(
167144
]
168145
assert send_email.delay.calls == [
169146
pretend.call(
170-
'Email Body',
171147
'Email Subject',
172-
recipients=[stub_user.email],
148+
'Email Body',
149+
recipient=stub_user.email,
173150
),
174151
]
175152

@@ -232,9 +209,9 @@ def test_email_verification_email(
232209
]
233210
assert send_email.delay.calls == [
234211
pretend.call(
235-
'Email Body',
236212
'Email Subject',
237-
recipients=[stub_email.email],
213+
'Email Body',
214+
recipient=stub_email.email,
238215
),
239216
]
240217

@@ -280,9 +257,9 @@ def test_password_change_email(
280257
]
281258
assert send_email.delay.calls == [
282259
pretend.call(
283-
'Email Body',
284260
'Email Subject',
285-
recipients=[stub_user.email],
261+
'Email Body',
262+
recipient=stub_user.email,
286263
),
287264
]
288265

@@ -328,9 +305,9 @@ def test_account_deletion_email(
328305
]
329306
assert send_email.delay.calls == [
330307
pretend.call(
331-
'Email Body',
332308
'Email Subject',
333-
recipients=[stub_user.email],
309+
'Email Body',
310+
recipient=stub_user.email,
334311
),
335312
]
336313

@@ -379,9 +356,9 @@ def test_primary_email_change_email(
379356
]
380357
assert send_email.delay.calls == [
381358
pretend.call(
382-
'Email Body',
383359
'Email Subject',
384-
recipients=['old_email'],
360+
'Email Body',
361+
recipient='old_email',
385362
),
386363
]
387364

@@ -439,12 +416,18 @@ def test_collaborator_added_email(
439416

440417
assert pyramid_request.task.calls == [
441418
pretend.call(send_email),
419+
pretend.call(send_email),
442420
]
443421
assert send_email.delay.calls == [
444422
pretend.call(
423+
'Email Subject',
445424
'Email Body',
425+
recipient=stub_user.email,
426+
),
427+
pretend.call(
446428
'Email Subject',
447-
bcc=[stub_user.email, stub_submitter_user.email],
429+
'Email Body',
430+
recipient=stub_submitter_user.email,
448431
),
449432
]
450433

@@ -502,8 +485,8 @@ def test_added_as_collaborator_email(
502485
]
503486
assert send_email.delay.calls == [
504487
pretend.call(
505-
'Email Body',
506488
'Email Subject',
507-
recipients=[stub_user.email],
489+
'Email Body',
490+
recipient=stub_user.email,
508491
),
509492
]

tests/unit/email/test_services.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import pretend
14+
15+
from pyramid_mailer.mailer import DummyMailer
16+
from zope.interface.verify import verifyClass
17+
18+
from warehouse.email.interfaces import IEmailSender
19+
from warehouse.email.services import SMTPEmailSender
20+
21+
22+
class TestSMTPEmailSender:
23+
24+
def test_verify_service(self):
25+
assert verifyClass(IEmailSender, SMTPEmailSender)
26+
27+
def test_creates_service(self):
28+
mailer = pretend.stub()
29+
context = pretend.stub()
30+
request = pretend.stub(
31+
registry=pretend.stub(
32+
settings=pretend.stub(get=lambda k: "SENDER"),
33+
getUtility=lambda mailr: mailer,
34+
)
35+
)
36+
37+
service = SMTPEmailSender.create_service(context, request)
38+
39+
assert isinstance(service, SMTPEmailSender)
40+
assert service.mailer is mailer
41+
assert service.sender == "SENDER"
42+
43+
def test_send(self):
44+
mailer = DummyMailer()
45+
service = SMTPEmailSender(mailer, sender="[email protected]")
46+
47+
service.send("a subject", "a body", recipient="[email protected]")
48+
49+
assert len(mailer.outbox) == 1
50+
51+
msg = mailer.outbox[0]
52+
53+
assert msg.subject == "a subject"
54+
assert msg.body == "a body"
55+
assert msg.recipients == ["[email protected]"]
56+
assert msg.sender == "[email protected]"

tests/unit/test_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ def __init__(self):
325325
pretend.call("pyramid_rpc.xmlrpc"),
326326
pretend.call(".legacy.action_routing"),
327327
pretend.call(".domain"),
328+
pretend.call(".email"),
328329
pretend.call(".i18n"),
329330
pretend.call(".db"),
330331
pretend.call(".tasks"),

warehouse/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ def configure(settings=None):
344344
# Register support for template views.
345345
config.add_directive("add_template_view", template_view, action_wrap=False)
346346

347+
# Register support for sendnging emails
348+
config.include(".email")
349+
347350
# Register support for internationalization and localization
348351
config.include(".i18n")
349352

warehouse/email.py renamed to warehouse/email/__init__.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,19 @@
1111
# limitations under the License.
1212

1313
from pyramid.renderers import render
14-
from pyramid_mailer import get_mailer
15-
from pyramid_mailer.message import Message
1614

1715
from warehouse import tasks
1816
from warehouse.accounts.interfaces import ITokenService
17+
from warehouse.email.interfaces import IEmailSender
18+
from warehouse.email.services import SMTPEmailSender
1919

2020

2121
@tasks.task(bind=True, ignore_result=True, acks_late=True)
22-
def send_email(task, request, body, subject, *, recipients=None, bcc=None):
23-
24-
mailer = get_mailer(request)
25-
message = Message(
26-
body=body,
27-
recipients=recipients,
28-
bcc=bcc,
29-
sender=request.registry.settings.get('mail.sender'),
30-
subject=subject
31-
)
22+
def send_email(task, request, subject, body, *, recipient=None):
23+
sender = request.find_service(IEmailSender)
24+
3225
try:
33-
mailer.send_immediately(message)
26+
sender.send(subject, body, recipient=recipient)
3427
except Exception as exc:
3528
task.retry(exc=exc)
3629

@@ -58,7 +51,7 @@ def send_password_reset_email(request, user):
5851
'email/password-reset.body.txt', fields, request=request
5952
)
6053

61-
request.task(send_email).delay(body, subject, recipients=[user.email])
54+
request.task(send_email).delay(subject, body, recipient=user.email)
6255

6356
# Return the fields we used, in case we need to show any of them to the
6457
# user
@@ -87,7 +80,7 @@ def send_email_verification_email(request, email):
8780
'email/verify-email.body.txt', fields, request=request
8881
)
8982

90-
request.task(send_email).delay(body, subject, recipients=[email.email])
83+
request.task(send_email).delay(subject, body, recipient=email.email)
9184

9285
return fields
9386

@@ -105,7 +98,7 @@ def send_password_change_email(request, user):
10598
'email/password-change.body.txt', fields, request=request
10699
)
107100

108-
request.task(send_email).delay(body, subject, recipients=[user.email])
101+
request.task(send_email).delay(subject, body, recipient=user.email)
109102

110103
return fields
111104

@@ -123,7 +116,7 @@ def send_account_deletion_email(request, user):
123116
'email/account-deleted.body.txt', fields, request=request
124117
)
125118

126-
request.task(send_email).delay(body, subject, recipients=[user.email])
119+
request.task(send_email).delay(subject, body, recipient=user.email)
127120

128121
return fields
129122

@@ -143,7 +136,7 @@ def send_primary_email_change_email(request, user, email):
143136
'email/primary-email-change.body.txt', fields, request=request
144137
)
145138

146-
request.task(send_email).delay(body, subject, recipients=[email])
139+
request.task(send_email).delay(subject, body, recipient=email)
147140

148141
return fields
149142

@@ -165,7 +158,8 @@ def send_collaborator_added_email(request, user, submitter, project_name, role,
165158
'email/collaborator-added.body.txt', fields, request=request
166159
)
167160

168-
request.task(send_email).delay(body, subject, bcc=email_recipients)
161+
for recipient in email_recipients:
162+
request.task(send_email).delay(subject, body, recipient=recipient)
169163

170164
return fields
171165

@@ -186,6 +180,13 @@ def send_added_as_collaborator_email(request, submitter, project_name, role,
186180
'email/added-as-collaborator.body.txt', fields, request=request
187181
)
188182

189-
request.task(send_email).delay(body, subject, recipients=[user_email])
183+
request.task(send_email).delay(subject, body, recipient=user_email)
190184

191185
return fields
186+
187+
188+
def includeme(config):
189+
config.register_service_factory(
190+
SMTPEmailSender.create_service,
191+
IEmailSender,
192+
)

0 commit comments

Comments
 (0)