PageRenderTime 81ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/mailman/app/tests/test_bounces.py

https://gitlab.com/noc0lour/mailman
Python | 507 lines | 480 code | 8 blank | 19 comment | 0 complexity | d32bc687c41989ca32aa87e8f1445a96 MD5 | raw file
  1. # Copyright (C) 2011-2016 by the Free Software Foundation, Inc.
  2. #
  3. # This file is part of GNU Mailman.
  4. #
  5. # GNU Mailman is free software: you can redistribute it and/or modify it under
  6. # the terms of the GNU General Public License as published by the Free
  7. # Software Foundation, either version 3 of the License, or (at your option)
  8. # any later version.
  9. #
  10. # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  12. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  13. # more details.
  14. #
  15. # You should have received a copy of the GNU General Public License along with
  16. # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
  17. """Testing app.bounces functions."""
  18. import os
  19. import uuid
  20. import shutil
  21. import tempfile
  22. import unittest
  23. from mailman.app.bounces import (
  24. ProbeVERP, StandardVERP, bounce_message, maybe_forward, send_probe)
  25. from mailman.app.lifecycle import create_list
  26. from mailman.config import config
  27. from mailman.interfaces.bounce import UnrecognizedBounceDisposition
  28. from mailman.interfaces.languages import ILanguageManager
  29. from mailman.interfaces.member import MemberRole
  30. from mailman.interfaces.pending import IPendings
  31. from mailman.interfaces.usermanager import IUserManager
  32. from mailman.testing.helpers import (
  33. LogFileMark, get_queue_messages, specialized_message_from_string as mfs,
  34. subscribe)
  35. from mailman.testing.layers import ConfigLayer
  36. from zope.component import getUtility
  37. class TestVERP(unittest.TestCase):
  38. """Test header VERP detection."""
  39. layer = ConfigLayer
  40. def setUp(self):
  41. self._mlist = create_list('test@example.com')
  42. self._verper = StandardVERP()
  43. def test_no_verp(self):
  44. # The empty set is returned when there is no VERP headers.
  45. msg = mfs("""\
  46. From: postmaster@example.com
  47. To: mailman-bounces@example.com
  48. """)
  49. self.assertEqual(self._verper.get_verp(self._mlist, msg), set())
  50. def test_verp_in_to(self):
  51. # A VERP address is found in the To header.
  52. msg = mfs("""\
  53. From: postmaster@example.com
  54. To: test-bounces+anne=example.org@example.com
  55. """)
  56. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  57. set(['anne@example.org']))
  58. def test_verp_in_delivered_to(self):
  59. # A VERP address is found in the Delivered-To header.
  60. msg = mfs("""\
  61. From: postmaster@example.com
  62. Delivered-To: test-bounces+anne=example.org@example.com
  63. """)
  64. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  65. set(['anne@example.org']))
  66. def test_verp_in_envelope_to(self):
  67. # A VERP address is found in the Envelope-To header.
  68. msg = mfs("""\
  69. From: postmaster@example.com
  70. Envelope-To: test-bounces+anne=example.org@example.com
  71. """)
  72. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  73. set(['anne@example.org']))
  74. def test_verp_in_apparently_to(self):
  75. # A VERP address is found in the Apparently-To header.
  76. msg = mfs("""\
  77. From: postmaster@example.com
  78. Apparently-To: test-bounces+anne=example.org@example.com
  79. """)
  80. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  81. set(['anne@example.org']))
  82. def test_verp_with_empty_header(self):
  83. # A VERP address is found, but there's an empty header.
  84. msg = mfs("""\
  85. From: postmaster@example.com
  86. To: test-bounces+anne=example.org@example.com
  87. To:
  88. """)
  89. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  90. set(['anne@example.org']))
  91. def test_no_verp_with_empty_header(self):
  92. # There's an empty header, and no VERP address is found.
  93. msg = mfs("""\
  94. From: postmaster@example.com
  95. To:
  96. """)
  97. self.assertEqual(self._verper.get_verp(self._mlist, msg), set())
  98. def test_verp_with_non_match(self):
  99. # A VERP address is found, but a header had a non-matching pattern.
  100. msg = mfs("""\
  101. From: postmaster@example.com
  102. To: test-bounces+anne=example.org@example.com
  103. To: test-bounces@example.com
  104. """)
  105. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  106. set(['anne@example.org']))
  107. def test_no_verp_with_non_match(self):
  108. # No VERP address is found, and a header had a non-matching pattern.
  109. msg = mfs("""\
  110. From: postmaster@example.com
  111. To: test-bounces@example.com
  112. """)
  113. self.assertEqual(self._verper.get_verp(self._mlist, msg), set())
  114. def test_multiple_verps(self):
  115. # More than one VERP address was found in the same header.
  116. msg = mfs("""\
  117. From: postmaster@example.com
  118. To: test-bounces+anne=example.org@example.com
  119. To: test-bounces+anne=example.org@example.com
  120. """)
  121. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  122. set(['anne@example.org']))
  123. def test_multiple_verps_different_values(self):
  124. # More than one VERP address was found in the same header with
  125. # different values.
  126. msg = mfs("""\
  127. From: postmaster@example.com
  128. To: test-bounces+anne=example.org@example.com
  129. To: test-bounces+bart=example.org@example.com
  130. """)
  131. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  132. set(['anne@example.org', 'bart@example.org']))
  133. def test_multiple_verps_different_values_different_headers(self):
  134. # More than one VERP address was found in different headers with
  135. # different values.
  136. msg = mfs("""\
  137. From: postmaster@example.com
  138. To: test-bounces+anne=example.org@example.com
  139. Apparently-To: test-bounces+bart=example.org@example.com
  140. """)
  141. self.assertEqual(self._verper.get_verp(self._mlist, msg),
  142. set(['anne@example.org', 'bart@example.org']))
  143. class TestSendProbe(unittest.TestCase):
  144. """Test sending of the probe message."""
  145. layer = ConfigLayer
  146. def setUp(self):
  147. self._mlist = create_list('test@example.com')
  148. self._mlist.send_welcome_message = False
  149. self._member = subscribe(self._mlist, 'Anne', email='anne@example.com')
  150. self._msg = mfs("""\
  151. From: bouncer@example.com
  152. To: anne@example.com
  153. Subject: You bounced
  154. Message-ID: <first>
  155. """)
  156. def test_token(self):
  157. # Show that send_probe() returns a proper token, and that the token
  158. # corresponds to a record in the pending database.
  159. token = send_probe(self._member, self._msg)
  160. pendable = getUtility(IPendings).confirm(token)
  161. self.assertEqual(len(pendable.items()), 3)
  162. self.assertEqual(set(pendable.keys()),
  163. set(['member_id', 'message_id', 'type']))
  164. # member_ids are pended as unicodes.
  165. self.assertEqual(uuid.UUID(hex=pendable['member_id']),
  166. self._member.member_id)
  167. self.assertEqual(pendable['message_id'], '<first>')
  168. def test_probe_is_multipart(self):
  169. # The probe is a multipart/mixed with two subparts.
  170. send_probe(self._member, self._msg)
  171. items = get_queue_messages('virgin', expected_count=1)
  172. message = items[0].msg
  173. self.assertEqual(message.get_content_type(), 'multipart/mixed')
  174. self.assertTrue(message.is_multipart())
  175. self.assertEqual(len(message.get_payload()), 2)
  176. def test_probe_sends_one_message(self):
  177. # send_probe() places one message in the virgin queue. We start out
  178. # with no messages in the queue.
  179. get_queue_messages('virgin', expected_count=0)
  180. send_probe(self._member, self._msg)
  181. get_queue_messages('virgin', expected_count=1)
  182. def test_probe_contains_original(self):
  183. # Show that send_probe() places a properly formatted message in the
  184. # virgin queue.
  185. send_probe(self._member, self._msg)
  186. items = get_queue_messages('virgin', expected_count=1)
  187. rfc822 = items[0].msg.get_payload(1)
  188. self.assertEqual(rfc822.get_content_type(), 'message/rfc822')
  189. self.assertTrue(rfc822.is_multipart())
  190. self.assertEqual(len(rfc822.get_payload()), 1)
  191. self.assertEqual(rfc822.get_payload(0).as_string(),
  192. self._msg.as_string())
  193. def test_notice(self):
  194. # Test that the notice in the first subpart is correct.
  195. send_probe(self._member, self._msg)
  196. items = get_queue_messages('virgin', expected_count=1)
  197. message = items[0].msg
  198. notice = message.get_payload(0)
  199. self.assertEqual(notice.get_content_type(), 'text/plain')
  200. # The interesting bits are the parts that have been interpolated into
  201. # the message. For now the best we can do is know that the
  202. # interpolation values appear in the message. When Python 2.7 is our
  203. # minimum requirement, we can use assertRegexpMatches().
  204. body = notice.get_payload()
  205. self.assertIn('test@example.com', body)
  206. self.assertIn('anne@example.com', body)
  207. self.assertIn('http://example.com/anne@example.com', body)
  208. self.assertIn('test-owner@example.com', body)
  209. def test_headers(self):
  210. # Check the headers of the outer message.
  211. token = send_probe(self._member, self._msg)
  212. items = get_queue_messages('virgin', expected_count=1)
  213. message = items[0].msg
  214. self.assertEqual(message['from'],
  215. 'test-bounces+{0}@example.com'.format(token))
  216. self.assertEqual(message['to'], 'anne@example.com')
  217. self.assertEqual(message['subject'], 'Test mailing list probe message')
  218. def test_no_precedence_header(self):
  219. # Probe messages should not have a Precedence header (LP: #808821).
  220. send_probe(self._member, self._msg)
  221. items = get_queue_messages('virgin', expected_count=1)
  222. self.assertIsNone(items[0].msg['precedence'])
  223. class TestSendProbeNonEnglish(unittest.TestCase):
  224. """Test sending of the probe message to a non-English speaker."""
  225. layer = ConfigLayer
  226. maxDiff = None
  227. def setUp(self):
  228. self._mlist = create_list('test@example.com')
  229. self._member = subscribe(self._mlist, 'Anne', email='anne@example.com')
  230. self._msg = mfs("""\
  231. From: bouncer@example.com
  232. To: anne@example.com
  233. Subject: You bounced
  234. Message-ID: <first>
  235. """)
  236. # Set up the translation context.
  237. self._var_dir = tempfile.mkdtemp()
  238. self.addCleanup(shutil.rmtree, self._var_dir)
  239. xx_template_path = os.path.join(
  240. self._var_dir, 'templates', 'site', 'xx', 'probe.txt')
  241. os.makedirs(os.path.dirname(xx_template_path))
  242. config.push('xx template dir', """\
  243. [paths.testing]
  244. var_dir: {}
  245. """.format(self._var_dir))
  246. self.addCleanup(config.pop, 'xx template dir')
  247. language_manager = getUtility(ILanguageManager)
  248. language_manager.add('xx', 'utf-8', 'Freedonia')
  249. self._member.preferences.preferred_language = 'xx'
  250. with open(xx_template_path, 'w') as fp:
  251. print("""\
  252. blah blah blah
  253. $listname
  254. $address
  255. $optionsurl
  256. $owneraddr
  257. """, file=fp)
  258. def test_subject_with_member_nonenglish(self):
  259. # Test that members with non-English preferred language get a Subject
  260. # header in the expected language.
  261. send_probe(self._member, self._msg)
  262. items = get_queue_messages('virgin', expected_count=1)
  263. self.assertEqual(
  264. items[0].msg['subject'].encode(),
  265. '=?utf-8?q?ailing-may_ist-lay_Test_obe-pray_essage-may?=')
  266. def test_probe_notice_with_member_nonenglish(self):
  267. # Test that a member with non-English preferred language gets the
  268. # probe message in their language.
  269. send_probe(self._member, self._msg)
  270. items = get_queue_messages('virgin', expected_count=1)
  271. message = items[0].msg
  272. notice = message.get_payload(0).get_payload()
  273. self.assertMultiLineEqual(notice, """\
  274. blah blah blah test@example.com anne@example.com
  275. http://example.com/anne@example.com test-owner@example.com""")
  276. class TestProbe(unittest.TestCase):
  277. """Test VERP probe parsing."""
  278. layer = ConfigLayer
  279. def setUp(self):
  280. self._mlist = create_list('test@example.com')
  281. self._mlist.send_welcome_message = False
  282. self._member = subscribe(self._mlist, 'Anne', email='anne@example.com')
  283. self._msg = mfs("""\
  284. From: bouncer@example.com
  285. To: anne@example.com
  286. Subject: You bounced
  287. Message-ID: <first>
  288. """)
  289. def test_get_addresses(self):
  290. # Be able to extract the probed address from the pending database
  291. # based on the token in a probe bounce.
  292. token = send_probe(self._member, self._msg)
  293. # Simulate a bounce of the message in the virgin queue.
  294. items = get_queue_messages('virgin', expected_count=1)
  295. message = items[0].msg
  296. bounce = mfs("""\
  297. To: {0}
  298. From: mail-daemon@example.com
  299. """.format(message['From']))
  300. addresses = ProbeVERP().get_verp(self._mlist, bounce)
  301. self.assertEqual(addresses, set(['anne@example.com']))
  302. # The pendable is no longer in the database.
  303. self.assertIsNone(getUtility(IPendings).confirm(token))
  304. class TestMaybeForward(unittest.TestCase):
  305. """Test forwarding of unrecognized bounces."""
  306. layer = ConfigLayer
  307. def setUp(self):
  308. config.push('test config', """
  309. [mailman]
  310. site_owner: postmaster@example.com
  311. """)
  312. self.addCleanup(config.pop, 'test config')
  313. self._mlist = create_list('test@example.com')
  314. self._mlist.send_welcome_message = False
  315. self._msg = mfs("""\
  316. From: bouncer@example.com
  317. To: test-bounces@example.com
  318. Subject: You bounced
  319. Message-ID: <first>
  320. """)
  321. def test_maybe_forward_discard(self):
  322. # When forward_unrecognized_bounces_to is set to discard, no bounce
  323. # messages are forwarded.
  324. self._mlist.forward_unrecognized_bounces_to = (
  325. UnrecognizedBounceDisposition.discard)
  326. # The only artifact of this call is a log file entry.
  327. mark = LogFileMark('mailman.bounce')
  328. maybe_forward(self._mlist, self._msg)
  329. get_queue_messages('virgin', expected_count=0)
  330. line = mark.readline()
  331. self.assertEqual(
  332. line[-40:-1],
  333. 'Discarding unrecognized bounce: <first>')
  334. def test_maybe_forward_list_owner(self):
  335. # Set up some owner and moderator addresses.
  336. user_manager = getUtility(IUserManager)
  337. anne = user_manager.create_address('anne@example.com')
  338. bart = user_manager.create_address('bart@example.com')
  339. cris = user_manager.create_address('cris@example.com')
  340. dave = user_manager.create_address('dave@example.com')
  341. # Regular members.
  342. elle = user_manager.create_address('elle@example.com')
  343. fred = user_manager.create_address('fred@example.com')
  344. self._mlist.subscribe(anne, MemberRole.owner)
  345. self._mlist.subscribe(bart, MemberRole.owner)
  346. self._mlist.subscribe(cris, MemberRole.moderator)
  347. self._mlist.subscribe(dave, MemberRole.moderator)
  348. self._mlist.subscribe(elle, MemberRole.member)
  349. self._mlist.subscribe(fred, MemberRole.member)
  350. # When forward_unrecognized_bounces_to is set to owners, the
  351. # bounce is forwarded to the list owners and moderators.
  352. self._mlist.forward_unrecognized_bounces_to = (
  353. UnrecognizedBounceDisposition.administrators)
  354. maybe_forward(self._mlist, self._msg)
  355. items = get_queue_messages('virgin', expected_count=1)
  356. msg = items[0].msg
  357. self.assertEqual(msg['subject'], 'Uncaught bounce notification')
  358. self.assertEqual(msg['from'], 'postmaster@example.com')
  359. self.assertEqual(msg['to'], 'test-owner@example.com')
  360. # The first attachment is a notification message with a url.
  361. payload = msg.get_payload(0)
  362. self.assertEqual(payload.get_content_type(), 'text/plain')
  363. body = payload.get_payload()
  364. self.assertEqual(
  365. body.splitlines()[-1],
  366. 'http://lists.example.com/admin/test@example.com/bounce')
  367. # The second attachment should be a message/rfc822 containing the
  368. # original bounce message.
  369. payload = msg.get_payload(1)
  370. self.assertEqual(payload.get_content_type(), 'message/rfc822')
  371. bounce = payload.get_payload(0)
  372. self.assertEqual(bounce.as_string(), self._msg.as_string())
  373. # All of the owners and moderators, but none of the members, should be
  374. # recipients of this message.
  375. self.assertEqual(items[0].msgdata['recipients'],
  376. set(['anne@example.com', 'bart@example.com',
  377. 'cris@example.com', 'dave@example.com']))
  378. def test_maybe_forward_site_owner(self):
  379. # Set up some owner and moderator addresses.
  380. user_manager = getUtility(IUserManager)
  381. anne = user_manager.create_address('anne@example.com')
  382. bart = user_manager.create_address('bart@example.com')
  383. cris = user_manager.create_address('cris@example.com')
  384. dave = user_manager.create_address('dave@example.com')
  385. # Regular members.
  386. elle = user_manager.create_address('elle@example.com')
  387. fred = user_manager.create_address('fred@example.com')
  388. self._mlist.subscribe(anne, MemberRole.owner)
  389. self._mlist.subscribe(bart, MemberRole.owner)
  390. self._mlist.subscribe(cris, MemberRole.moderator)
  391. self._mlist.subscribe(dave, MemberRole.moderator)
  392. self._mlist.subscribe(elle, MemberRole.member)
  393. self._mlist.subscribe(fred, MemberRole.member)
  394. # When forward_unrecognized_bounces_to is set to owners, the
  395. # bounce is forwarded to the list owners and moderators.
  396. self._mlist.forward_unrecognized_bounces_to = (
  397. UnrecognizedBounceDisposition.site_owner)
  398. maybe_forward(self._mlist, self._msg)
  399. items = get_queue_messages('virgin', expected_count=1)
  400. msg = items[0].msg
  401. self.assertEqual(msg['subject'], 'Uncaught bounce notification')
  402. self.assertEqual(msg['from'], 'postmaster@example.com')
  403. self.assertEqual(msg['to'], 'postmaster@example.com')
  404. # The first attachment is a notification message with a url.
  405. payload = msg.get_payload(0)
  406. self.assertEqual(payload.get_content_type(), 'text/plain')
  407. body = payload.get_payload()
  408. self.assertEqual(
  409. body.splitlines()[-1],
  410. 'http://lists.example.com/admin/test@example.com/bounce')
  411. # The second attachment should be a message/rfc822 containing the
  412. # original bounce message.
  413. payload = msg.get_payload(1)
  414. self.assertEqual(payload.get_content_type(), 'message/rfc822')
  415. bounce = payload.get_payload(0)
  416. self.assertEqual(bounce.as_string(), self._msg.as_string())
  417. # All of the owners and moderators, but none of the members, should be
  418. # recipients of this message.
  419. self.assertEqual(items[0].msgdata['recipients'],
  420. set(['postmaster@example.com']))
  421. class TestBounceMessage(unittest.TestCase):
  422. """Test the `mailman.app.bounces.bounce_message()` function."""
  423. layer = ConfigLayer
  424. def setUp(self):
  425. self._mlist = create_list('test@example.com')
  426. self._msg = mfs("""\
  427. From: anne@example.com
  428. To: test@example.com
  429. Subject: Ignore
  430. """)
  431. def test_no_sender(self):
  432. # The message won't be bounced if it has no discernible sender.
  433. del self._msg['from']
  434. bounce_message(self._mlist, self._msg)
  435. # Nothing in the virgin queue means nothing's been bounced.
  436. get_queue_messages('virgin', expected_count=0)