test_email.py 18 KB

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