You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

541 lines
20 KiB

  1. # Copyright 2018 New Vector
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import email.message
  15. import os
  16. from http import HTTPStatus
  17. from typing import Any, Dict, List, Sequence, Tuple
  18. import attr
  19. import pkg_resources
  20. from parameterized import parameterized
  21. from twisted.internet.defer import Deferred
  22. from twisted.test.proto_helpers import MemoryReactor
  23. import synapse.rest.admin
  24. from synapse.api.errors import Codes, SynapseError
  25. from synapse.push.emailpusher import EmailPusher
  26. from synapse.rest.client import login, room
  27. from synapse.rest.synapse.client.unsubscribe import UnsubscribeResource
  28. from synapse.server import HomeServer
  29. from synapse.util import Clock
  30. from tests.server import FakeSite, make_request
  31. from tests.unittest import HomeserverTestCase
  32. @attr.s(auto_attribs=True)
  33. class _User:
  34. "Helper wrapper for user ID and access token"
  35. id: str
  36. token: str
  37. class EmailPusherTests(HomeserverTestCase):
  38. servlets = [
  39. synapse.rest.admin.register_servlets_for_client_rest_resource,
  40. room.register_servlets,
  41. login.register_servlets,
  42. ]
  43. hijack_auth = False
  44. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  45. config = self.default_config()
  46. config["email"] = {
  47. "enable_notifs": True,
  48. "template_dir": os.path.abspath(
  49. pkg_resources.resource_filename("synapse", "res/templates")
  50. ),
  51. "expiry_template_html": "notice_expiry.html",
  52. "expiry_template_text": "notice_expiry.txt",
  53. "notif_template_html": "notif_mail.html",
  54. "notif_template_text": "notif_mail.txt",
  55. "smtp_host": "127.0.0.1",
  56. "smtp_port": 20,
  57. "require_transport_security": False,
  58. "smtp_user": None,
  59. "smtp_pass": None,
  60. "app_name": "Matrix",
  61. "notif_from": "test@example.com",
  62. "riot_base_url": None,
  63. }
  64. config["public_baseurl"] = "http://aaa"
  65. hs = self.setup_test_homeserver(config=config)
  66. # List[Tuple[Deferred, args, kwargs]]
  67. self.email_attempts: List[Tuple[Deferred, Sequence, Dict]] = []
  68. def sendmail(*args: Any, **kwargs: Any) -> Deferred:
  69. # This mocks out synapse.reactor.send_email._sendmail.
  70. d: Deferred = Deferred()
  71. self.email_attempts.append((d, args, kwargs))
  72. return d
  73. hs.get_send_email_handler()._sendmail = sendmail # type: ignore[assignment]
  74. return hs
  75. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  76. # Register the user who gets notified
  77. self.user_id = self.register_user("user", "pass")
  78. self.access_token = self.login("user", "pass")
  79. # Register other users
  80. self.others = [
  81. _User(
  82. id=self.register_user("otheruser1", "pass"),
  83. token=self.login("otheruser1", "pass"),
  84. ),
  85. _User(
  86. id=self.register_user("otheruser2", "pass"),
  87. token=self.login("otheruser2", "pass"),
  88. ),
  89. ]
  90. # Register the pusher
  91. user_tuple = self.get_success(
  92. self.hs.get_datastores().main.get_user_by_access_token(self.access_token)
  93. )
  94. assert user_tuple is not None
  95. self.device_id = user_tuple.device_id
  96. # We need to add email to account before we can create a pusher.
  97. self.get_success(
  98. hs.get_datastores().main.user_add_threepid(
  99. self.user_id, "email", "a@example.com", 0, 0
  100. )
  101. )
  102. pusher = self.get_success(
  103. self.hs.get_pusherpool().add_or_update_pusher(
  104. user_id=self.user_id,
  105. device_id=self.device_id,
  106. kind="email",
  107. app_id="m.email",
  108. app_display_name="Email Notifications",
  109. device_display_name="a@example.com",
  110. pushkey="a@example.com",
  111. lang=None,
  112. data={},
  113. )
  114. )
  115. assert isinstance(pusher, EmailPusher)
  116. self.pusher = pusher
  117. self.auth_handler = hs.get_auth_handler()
  118. self.store = hs.get_datastores().main
  119. def test_need_validated_email(self) -> None:
  120. """Test that we can only add an email pusher if the user has validated
  121. their email.
  122. """
  123. with self.assertRaises(SynapseError) as cm:
  124. self.get_success_or_raise(
  125. self.hs.get_pusherpool().add_or_update_pusher(
  126. user_id=self.user_id,
  127. device_id=self.device_id,
  128. kind="email",
  129. app_id="m.email",
  130. app_display_name="Email Notifications",
  131. device_display_name="b@example.com",
  132. pushkey="b@example.com",
  133. lang=None,
  134. data={},
  135. )
  136. )
  137. self.assertEqual(400, cm.exception.code)
  138. self.assertEqual(Codes.THREEPID_NOT_FOUND, cm.exception.errcode)
  139. def test_simple_sends_email(self) -> None:
  140. # Create a simple room with two users
  141. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  142. self.helper.invite(
  143. room=room, src=self.user_id, tok=self.access_token, targ=self.others[0].id
  144. )
  145. self.helper.join(room=room, user=self.others[0].id, tok=self.others[0].token)
  146. # The other user sends a single message.
  147. self.helper.send(room, body="Hi!", tok=self.others[0].token)
  148. # We should get emailed about that message
  149. self._check_for_mail()
  150. # The other user sends multiple messages.
  151. self.helper.send(room, body="Hi!", tok=self.others[0].token)
  152. self.helper.send(room, body="There!", tok=self.others[0].token)
  153. self._check_for_mail()
  154. @parameterized.expand([(False,), (True,)])
  155. def test_unsubscribe(self, use_post: bool) -> None:
  156. # Create a simple room with two users
  157. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  158. self.helper.invite(
  159. room=room, src=self.user_id, tok=self.access_token, targ=self.others[0].id
  160. )
  161. self.helper.join(room=room, user=self.others[0].id, tok=self.others[0].token)
  162. # The other user sends a single message.
  163. self.helper.send(room, body="Hi!", tok=self.others[0].token)
  164. # We should get emailed about that message
  165. args, kwargs = self._check_for_mail()
  166. # That email should contain an unsubscribe link in the body and header.
  167. msg: bytes = args[5]
  168. # Multipart: plain text, base 64 encoded; html, base 64 encoded
  169. multipart_msg = email.message_from_bytes(msg)
  170. txt = multipart_msg.get_payload()[0].get_payload(decode=True).decode()
  171. html = multipart_msg.get_payload()[1].get_payload(decode=True).decode()
  172. self.assertIn("/_synapse/client/unsubscribe", txt)
  173. self.assertIn("/_synapse/client/unsubscribe", html)
  174. # The unsubscribe headers should exist.
  175. assert multipart_msg.get("List-Unsubscribe") is not None
  176. self.assertIsNotNone(multipart_msg.get("List-Unsubscribe-Post"))
  177. # Open the unsubscribe link.
  178. unsubscribe_link = multipart_msg["List-Unsubscribe"].strip("<>")
  179. unsubscribe_resource = UnsubscribeResource(self.hs)
  180. channel = make_request(
  181. self.reactor,
  182. FakeSite(unsubscribe_resource, self.reactor),
  183. "POST" if use_post else "GET",
  184. unsubscribe_link,
  185. shorthand=False,
  186. )
  187. self.assertEqual(HTTPStatus.OK, channel.code, channel.result)
  188. # Ensure the pusher was removed.
  189. pushers = list(
  190. self.get_success(
  191. self.hs.get_datastores().main.get_pushers_by(
  192. {"user_name": self.user_id}
  193. )
  194. )
  195. )
  196. self.assertEqual(pushers, [])
  197. def test_invite_sends_email(self) -> None:
  198. # Create a room and invite the user to it
  199. room = self.helper.create_room_as(self.others[0].id, tok=self.others[0].token)
  200. self.helper.invite(
  201. room=room,
  202. src=self.others[0].id,
  203. tok=self.others[0].token,
  204. targ=self.user_id,
  205. )
  206. # We should get emailed about the invite
  207. self._check_for_mail()
  208. def test_invite_to_empty_room_sends_email(self) -> None:
  209. # Create a room and invite the user to it
  210. room = self.helper.create_room_as(self.others[0].id, tok=self.others[0].token)
  211. self.helper.invite(
  212. room=room,
  213. src=self.others[0].id,
  214. tok=self.others[0].token,
  215. targ=self.user_id,
  216. )
  217. # Then have the original user leave
  218. self.helper.leave(room, self.others[0].id, tok=self.others[0].token)
  219. # We should get emailed about the invite
  220. self._check_for_mail()
  221. def test_multiple_members_email(self) -> None:
  222. # We want to test multiple notifications, so we pause processing of push
  223. # while we send messages.
  224. self.pusher._pause_processing()
  225. # Create a simple room with multiple other users
  226. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  227. for other in self.others:
  228. self.helper.invite(
  229. room=room, src=self.user_id, tok=self.access_token, targ=other.id
  230. )
  231. self.helper.join(room=room, user=other.id, tok=other.token)
  232. # The other users send some messages
  233. self.helper.send(room, body="Hi!", tok=self.others[0].token)
  234. self.helper.send(room, body="There!", tok=self.others[1].token)
  235. self.helper.send(room, body="There!", tok=self.others[1].token)
  236. # Nothing should have happened yet, as we're paused.
  237. assert not self.email_attempts
  238. self.pusher._resume_processing()
  239. # We should get emailed about those messages
  240. self._check_for_mail()
  241. def test_multiple_rooms(self) -> None:
  242. # We want to test multiple notifications from multiple rooms, so we pause
  243. # processing of push while we send messages.
  244. self.pusher._pause_processing()
  245. # Create a simple room with multiple other users
  246. rooms = [
  247. self.helper.create_room_as(self.user_id, tok=self.access_token),
  248. self.helper.create_room_as(self.user_id, tok=self.access_token),
  249. ]
  250. for r, other in zip(rooms, self.others):
  251. self.helper.invite(
  252. room=r, src=self.user_id, tok=self.access_token, targ=other.id
  253. )
  254. self.helper.join(room=r, user=other.id, tok=other.token)
  255. # The other users send some messages
  256. self.helper.send(rooms[0], body="Hi!", tok=self.others[0].token)
  257. self.helper.send(rooms[1], body="There!", tok=self.others[1].token)
  258. self.helper.send(rooms[1], body="There!", tok=self.others[1].token)
  259. # Nothing should have happened yet, as we're paused.
  260. assert not self.email_attempts
  261. self.pusher._resume_processing()
  262. # We should get emailed about those messages
  263. self._check_for_mail()
  264. def test_room_notifications_include_avatar(self) -> None:
  265. # Create a room and set its avatar.
  266. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  267. self.helper.send_state(
  268. room, "m.room.avatar", {"url": "mxc://DUMMY_MEDIA_ID"}, self.access_token
  269. )
  270. # Invite two other uses.
  271. for other in self.others:
  272. self.helper.invite(
  273. room=room, src=self.user_id, tok=self.access_token, targ=other.id
  274. )
  275. self.helper.join(room=room, user=other.id, tok=other.token)
  276. # The other users send some messages.
  277. # TODO It seems that two messages are required to trigger an email?
  278. self.helper.send(room, body="Alpha", tok=self.others[0].token)
  279. self.helper.send(room, body="Beta", tok=self.others[1].token)
  280. # We should get emailed about those messages
  281. args, kwargs = self._check_for_mail()
  282. # That email should contain the room's avatar
  283. msg: bytes = args[5]
  284. # Multipart: plain text, base 64 encoded; html, base 64 encoded
  285. html = (
  286. email.message_from_bytes(msg)
  287. .get_payload()[1]
  288. .get_payload(decode=True)
  289. .decode()
  290. )
  291. self.assertIn("_matrix/media/v1/thumbnail/DUMMY_MEDIA_ID", html)
  292. def test_empty_room(self) -> None:
  293. """All users leaving a room shouldn't cause the pusher to break."""
  294. # Create a simple room with two users
  295. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  296. self.helper.invite(
  297. room=room, src=self.user_id, tok=self.access_token, targ=self.others[0].id
  298. )
  299. self.helper.join(room=room, user=self.others[0].id, tok=self.others[0].token)
  300. # The other user sends a single message.
  301. self.helper.send(room, body="Hi!", tok=self.others[0].token)
  302. # Leave the room before the message is processed.
  303. self.helper.leave(room, self.user_id, tok=self.access_token)
  304. self.helper.leave(room, self.others[0].id, tok=self.others[0].token)
  305. # We should get emailed about that message
  306. self._check_for_mail()
  307. def test_empty_room_multiple_messages(self) -> None:
  308. """All users leaving a room shouldn't cause the pusher to break."""
  309. # Create a simple room with two users
  310. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  311. self.helper.invite(
  312. room=room, src=self.user_id, tok=self.access_token, targ=self.others[0].id
  313. )
  314. self.helper.join(room=room, user=self.others[0].id, tok=self.others[0].token)
  315. # The other user sends a single message.
  316. self.helper.send(room, body="Hi!", tok=self.others[0].token)
  317. self.helper.send(room, body="There!", tok=self.others[0].token)
  318. # Leave the room before the message is processed.
  319. self.helper.leave(room, self.user_id, tok=self.access_token)
  320. self.helper.leave(room, self.others[0].id, tok=self.others[0].token)
  321. # We should get emailed about that message
  322. self._check_for_mail()
  323. def test_encrypted_message(self) -> None:
  324. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  325. self.helper.invite(
  326. room=room, src=self.user_id, tok=self.access_token, targ=self.others[0].id
  327. )
  328. self.helper.join(room=room, user=self.others[0].id, tok=self.others[0].token)
  329. # The other user sends some messages
  330. self.helper.send_event(room, "m.room.encrypted", {}, tok=self.others[0].token)
  331. # We should get emailed about that message
  332. self._check_for_mail()
  333. def test_no_email_sent_after_removed(self) -> None:
  334. # Create a simple room with two users
  335. room = self.helper.create_room_as(self.user_id, tok=self.access_token)
  336. self.helper.invite(
  337. room=room,
  338. src=self.user_id,
  339. tok=self.access_token,
  340. targ=self.others[0].id,
  341. )
  342. self.helper.join(
  343. room=room,
  344. user=self.others[0].id,
  345. tok=self.others[0].token,
  346. )
  347. # The other user sends a single message.
  348. self.helper.send(room, body="Hi!", tok=self.others[0].token)
  349. # We should get emailed about that message
  350. self._check_for_mail()
  351. # disassociate the user's email address
  352. self.get_success(
  353. self.auth_handler.delete_local_threepid(
  354. user_id=self.user_id, medium="email", address="a@example.com"
  355. )
  356. )
  357. # check that the pusher for that email address has been deleted
  358. pushers = list(
  359. self.get_success(
  360. self.hs.get_datastores().main.get_pushers_by(
  361. {"user_name": self.user_id}
  362. )
  363. )
  364. )
  365. self.assertEqual(len(pushers), 0)
  366. def test_remove_unlinked_pushers_background_job(self) -> None:
  367. """Checks that all existing pushers associated with unlinked email addresses are removed
  368. upon running the remove_deleted_email_pushers background update.
  369. """
  370. # disassociate the user's email address manually (without deleting the pusher).
  371. # This resembles the old behaviour, which the background update below is intended
  372. # to clean up.
  373. self.get_success(
  374. self.hs.get_datastores().main.user_delete_threepid(
  375. self.user_id, "email", "a@example.com"
  376. )
  377. )
  378. # Run the "remove_deleted_email_pushers" background job
  379. self.get_success(
  380. self.hs.get_datastores().main.db_pool.simple_insert(
  381. table="background_updates",
  382. values={
  383. "update_name": "remove_deleted_email_pushers",
  384. "progress_json": "{}",
  385. "depends_on": None,
  386. },
  387. )
  388. )
  389. # ... and tell the DataStore that it hasn't finished all updates yet
  390. self.hs.get_datastores().main.db_pool.updates._all_done = False
  391. # Now let's actually drive the updates to completion
  392. self.wait_for_background_updates()
  393. # Check that all pushers with unlinked addresses were deleted
  394. pushers = list(
  395. self.get_success(
  396. self.hs.get_datastores().main.get_pushers_by(
  397. {"user_name": self.user_id}
  398. )
  399. )
  400. )
  401. self.assertEqual(len(pushers), 0)
  402. def _check_for_mail(self) -> Tuple[Sequence, Dict]:
  403. """
  404. Assert that synapse sent off exactly one email notification.
  405. Returns:
  406. args and kwargs passed to synapse.reactor.send_email._sendmail for
  407. that notification.
  408. """
  409. # Get the stream ordering before it gets sent
  410. pushers = list(
  411. self.get_success(
  412. self.hs.get_datastores().main.get_pushers_by(
  413. {"user_name": self.user_id}
  414. )
  415. )
  416. )
  417. self.assertEqual(len(pushers), 1)
  418. last_stream_ordering = pushers[0].last_stream_ordering
  419. # Advance time a bit, so the pusher will register something has happened
  420. self.pump(10)
  421. # It hasn't succeeded yet, so the stream ordering shouldn't have moved
  422. pushers = list(
  423. self.get_success(
  424. self.hs.get_datastores().main.get_pushers_by(
  425. {"user_name": self.user_id}
  426. )
  427. )
  428. )
  429. self.assertEqual(len(pushers), 1)
  430. self.assertEqual(last_stream_ordering, pushers[0].last_stream_ordering)
  431. # One email was attempted to be sent
  432. self.assertEqual(len(self.email_attempts), 1)
  433. deferred, sendmail_args, sendmail_kwargs = self.email_attempts[0]
  434. # Make the email succeed
  435. deferred.callback(True)
  436. self.pump()
  437. # One email was attempted to be sent
  438. self.assertEqual(len(self.email_attempts), 1)
  439. # The stream ordering has increased
  440. pushers = list(
  441. self.get_success(
  442. self.hs.get_datastores().main.get_pushers_by(
  443. {"user_name": self.user_id}
  444. )
  445. )
  446. )
  447. self.assertEqual(len(pushers), 1)
  448. self.assertTrue(pushers[0].last_stream_ordering > last_stream_ordering)
  449. # Reset the attempts.
  450. self.email_attempts = []
  451. return sendmail_args, sendmail_kwargs