test_email.py 17 KB

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