test_sieve.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. #!/usr/bin/env python3
  2. """Tests for the ManageSieve API."""
  3. #
  4. # Copyright 2024 Odin Kroeger
  5. #
  6. # This file is part of SieveManager.
  7. #
  8. # SieveManager is free software: you can redistribute it and/or
  9. # modify it under the terms of the GNU General Public License as
  10. # published by the Free Software Foundation, either version 3 of
  11. # the License, or (at your option) any later version.
  12. #
  13. # SieveManager is distributed in the hope that it will be useful,
  14. # but WITHOUT ALL WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with SieveManager. If not, see <https://www.gnu.org/licenses/>.
  20. #
  21. # pylint: disable=missing-class-docstring,missing-function-docstring
  22. #
  23. # Modules
  24. #
  25. import os
  26. import pathlib
  27. import re
  28. import sys
  29. import unittest
  30. from typing import Final
  31. sys.path.append(os.path.realpath(pathlib.Path(__file__).parents[1]))
  32. from sievemgr import (Atom, Capabilities, Response, SieveManager,
  33. SieveError, SieveConnectionError,
  34. SieveOperationError, SieveProtocolError, URL)
  35. from . import Test, makerunner
  36. #
  37. # Globals
  38. #
  39. SCRIPTNAMES: Final[tuple[Test[bool], ...]] = (
  40. # Simple tests
  41. (('',), False),
  42. *[((chr(i),), False) for i in range(32)],
  43. (('\0x7f',), False),
  44. *[((chr(i),), False) for i in range(0x80, 0xa0)],
  45. (('\0x2028',), False),
  46. (('\0x2029',), False),
  47. (('\0xfeff',), False),
  48. (('\n',), False),
  49. (('\r',), False),
  50. ('\0', False),
  51. # Paradigmatic cases
  52. (('foo',), True),
  53. (('bar.sieve',), True),
  54. # Edge cases
  55. ((' ',), True),
  56. # Non-ASCII characters
  57. (('𝚏õȫ',), True),
  58. (('𝕗ōȏ.ⓢℹéṽ𝕖',), True)
  59. )
  60. SCRIPTNAME_ERRORS: Final[tuple[Test[bool], ...]] = tuple(
  61. ((*args, True), exp if exp else ValueError) for args, exp in SCRIPTNAMES
  62. )
  63. CAPABILITIES: Final[tuple[Test[Capabilities], ...]] = (
  64. # Simple cases
  65. (([],), Capabilities()),
  66. (([['IMPLEMENTATION', '']],), Capabilities(implementation='')),
  67. (([['IMPLEMENTATION', 'Foo v1']],), Capabilities(implementation='Foo v1')),
  68. (([['SASL', '']],), Capabilities()),
  69. (([['SASL', 'FOO']],), Capabilities(sasl=('foo',))),
  70. (([['SASL', 'FOO BAR']],), Capabilities(sasl=('foo', 'bar'))),
  71. (([['SIEVE', '']],), Capabilities()),
  72. (([['SIEVE', 'foo']],), Capabilities(sieve=('foo',))),
  73. (([['SIEVE', 'foo bar']],), Capabilities(sieve=('foo', 'bar'))),
  74. (([['STARTTLS']],), Capabilities(starttls=True)),
  75. (([['MAXREDIRECTS', '0']],), Capabilities(maxredirects=0)),
  76. (([['MAXREDIRECTS', '1']],), Capabilities(maxredirects=1)),
  77. (([['MAXREDIRECTS', '1']],), Capabilities(maxredirects=1)),
  78. (([['MAXREDIRECTS', '4294967295']],),
  79. Capabilities(maxredirects=4294967295)),
  80. (([['NOTIFY', '']],), Capabilities()),
  81. (([['NOTIFY', 'foo']],), Capabilities(notify=('foo',))),
  82. (([['NOTIFY', 'foo bar']],), Capabilities(notify=('foo', 'bar'))),
  83. (([['OWNER', 'user']],), Capabilities(owner='user')),
  84. (([['VERSION', '1.0']],), Capabilities(version='1.0')),
  85. (([['UNAUTHENTICATE']],), Capabilities(unauthenticate=True)),
  86. # Examples from RFC 5804
  87. (
  88. ([
  89. ['IMPlemENTATION', 'Example1 ManageSieved v001'],
  90. ['SASl', 'DIGEST-MD5 GSSAPI'],
  91. ['SIeVE', 'fileinto vacation'],
  92. ['StaRTTLS'],
  93. ['NOTIFY', 'xmpp mailto'],
  94. ['MAXREdIRECTS', '5'],
  95. ['VERSION', '1.0']
  96. ],),
  97. Capabilities(
  98. implementation='Example1 ManageSieved v001',
  99. sasl=('digest-md5', 'gssapi'),
  100. sieve=('fileinto', 'vacation'),
  101. starttls=True,
  102. notify=('xmpp', 'mailto'),
  103. maxredirects=5,
  104. version='1.0'
  105. )
  106. ),
  107. (
  108. ([
  109. ['IMPlemENTATION', 'Example1 ManageSieved v001'],
  110. ['SASl', 'DIGEST-MD5 GSSAPI'],
  111. ['SIeVE', 'fileinto vacation'],
  112. ['StaRTTLS'],
  113. ['NOTIFY', 'xmpp mailto'],
  114. ['MAXREdIRECTS', '5'],
  115. ['OWNER', 'alexey@example.com'],
  116. ['VERSION', '1.0']
  117. ],),
  118. Capabilities(
  119. implementation='Example1 ManageSieved v001',
  120. sasl=('digest-md5', 'gssapi'),
  121. sieve=('fileinto', 'vacation'),
  122. starttls=True,
  123. notify=('xmpp', 'mailto'),
  124. owner='alexey@example.com',
  125. maxredirects=5,
  126. version='1.0'
  127. )
  128. ),
  129. # Real-world data
  130. (
  131. ([
  132. ['IMPLEMENTATION', 'Dovecot Pigeonhole'],
  133. ['SIEVE', 'fileinto reject envelope encoded-character '
  134. 'vacation subaddress comparator-i;ascii-numeric '
  135. 'relational regex imap4flags copy include '
  136. 'variables body enotify environment '
  137. 'mailbox date index ihave duplicate '
  138. 'vacation-seconds imapflags notify '
  139. 'vnd.dovecot.pgp-encrypt editheader'],
  140. ['NOTIFY', 'mailto'],
  141. ['SASL', ''],
  142. ['STARTTLS'],
  143. ['VERSION', '1.0']
  144. ],),
  145. Capabilities(
  146. implementation='Dovecot Pigeonhole',
  147. sieve=('fileinto', 'reject', 'envelope', 'encoded-character',
  148. 'vacation', 'subaddress', 'comparator-i;ascii-numeric',
  149. 'relational', 'regex', 'imap4flags', 'copy', 'include',
  150. 'variables', 'body', 'enotify', 'environment',
  151. 'mailbox', 'date', 'index', 'ihave', 'duplicate',
  152. 'vacation-seconds', 'imapflags', 'notify',
  153. 'vnd.dovecot.pgp-encrypt', 'editheader'),
  154. notify=('mailto',),
  155. starttls=True,
  156. version='1.0'
  157. )
  158. ),
  159. (
  160. ([
  161. ['IMPLEMENTATION', 'Dovecot Pigeonhole'],
  162. ['SIEVE', 'fileinto reject envelope encoded-character '
  163. 'vacation subaddress comparator-i;ascii-numeric '
  164. 'relational regex imap4flags copy include '
  165. 'variables body enotify environment '
  166. 'mailbox date index ihave duplicate '
  167. 'vacation-seconds imapflags notify '
  168. 'vnd.dovecot.pgp-encrypt editheader'],
  169. ['NOTIFY', 'mailto'],
  170. ['SASL', 'PLAIN LOGIN OAUTHBEARER XOAUTH2'],
  171. ['VERSION', '1.0']
  172. ],),
  173. Capabilities(
  174. implementation='Dovecot Pigeonhole',
  175. sieve=('fileinto', 'reject', 'envelope', 'encoded-character',
  176. 'vacation', 'subaddress', 'comparator-i;ascii-numeric',
  177. 'relational', 'regex', 'imap4flags', 'copy', 'include',
  178. 'variables', 'body', 'enotify', 'environment',
  179. 'mailbox', 'date', 'index', 'ihave', 'duplicate',
  180. 'vacation-seconds', 'imapflags', 'notify',
  181. 'vnd.dovecot.pgp-encrypt', 'editheader'),
  182. notify=('mailto',),
  183. sasl=('plain', 'login', 'oauthbearer', 'xoauth2'),
  184. version='1.0'
  185. )
  186. ),
  187. (
  188. ([
  189. ['IMPLEMENTATION', 'Cyrus timsieved (Murder) 2.5.17'],
  190. ['SASL', 'PLAIN'],
  191. ['SIEVE', 'comparator-i;ascii-numeric fileinto reject '
  192. 'vacation vacation-seconds imapflags notify '
  193. 'envelope imap4flags relational regex '
  194. 'subaddress copy date'],
  195. ['UNAUTHENTICATE']
  196. ],),
  197. Capabilities(
  198. implementation='Cyrus timsieved (Murder) 2.5.17',
  199. sasl=('plain',),
  200. sieve=('comparator-i;ascii-numeric', 'fileinto', 'reject',
  201. 'vacation', 'vacation-seconds', 'imapflags', 'notify',
  202. 'envelope', 'imap4flags', 'relational', 'regex',
  203. 'subaddress', 'copy', 'date'),
  204. unauthenticate=True
  205. )
  206. ),
  207. # Errors
  208. (([[]],), SieveProtocolError),
  209. (([['IMPLEMENTATION']],), SieveProtocolError),
  210. (([['IMPLEMENTATION', None]],), SieveProtocolError),
  211. (([['IMPLEMENTATION', True]],), SieveProtocolError),
  212. (([['IMPLEMENTATION', 0]],), SieveProtocolError),
  213. (([['IMPLEMENTATION', 1]],), SieveProtocolError),
  214. (([['IMPLEMENTATION', []]],), SieveProtocolError),
  215. (([['IMPLEMENTATION', 'foo', 'bar']],), SieveProtocolError),
  216. (([['SASL']],), SieveProtocolError),
  217. (([['SASL', None]],), SieveProtocolError),
  218. (([['SASL', True]],), SieveProtocolError),
  219. (([['SASL', 0]],), SieveProtocolError),
  220. (([['SASL', 1]],), SieveProtocolError),
  221. (([['SASL', []]],), SieveProtocolError),
  222. (([['SASL', 'foo', 'bar']],), SieveProtocolError),
  223. (([['SIEVE']],), SieveProtocolError),
  224. (([['SIEVE', None]],), SieveProtocolError),
  225. (([['SIEVE', True]],), SieveProtocolError),
  226. (([['SIEVE', 0]],), SieveProtocolError),
  227. (([['SIEVE', 1]],), SieveProtocolError),
  228. (([['SIEVE', []]],), SieveProtocolError),
  229. (([['SIEVE', 'foo', 'bar']],), SieveProtocolError),
  230. (([['MAXREDIRECTS']],), SieveProtocolError),
  231. (([['MAXREDIRECTS', None]],), SieveProtocolError),
  232. (([['MAXREDIRECTS', True]],), SieveProtocolError),
  233. (([['MAXREDIRECTS', 0]],), SieveProtocolError),
  234. (([['MAXREDIRECTS', 1]],), SieveProtocolError),
  235. (([['MAXREDIRECTS', []]],), SieveProtocolError),
  236. (([['MAXREDIRECTS', 'foo', 'bar']],), SieveProtocolError),
  237. (([['NOTIFY']],), SieveProtocolError),
  238. (([['NOTIFY', None]],), SieveProtocolError),
  239. (([['NOTIFY', True]],), SieveProtocolError),
  240. (([['NOTIFY', 0]],), SieveProtocolError),
  241. (([['NOTIFY', 1]],), SieveProtocolError),
  242. (([['NOTIFY', []]],), SieveProtocolError),
  243. (([['NOTIFY', 'foo', 'bar']],), SieveProtocolError),
  244. (([['OWNER']],), SieveProtocolError),
  245. (([['OWNER', None]],), SieveProtocolError),
  246. (([['OWNER', True]],), SieveProtocolError),
  247. (([['OWNER', 0]],), SieveProtocolError),
  248. (([['OWNER', 1]],), SieveProtocolError),
  249. (([['OWNER', []]],), SieveProtocolError),
  250. (([['OWNER', 'foo', 'bar']],), SieveProtocolError),
  251. (([['VERSION']],), SieveProtocolError),
  252. (([['VERSION', None]],), SieveProtocolError),
  253. (([['VERSION', True]],), SieveProtocolError),
  254. (([['VERSION', 0]],), SieveProtocolError),
  255. (([['VERSION', 1]],), SieveProtocolError),
  256. (([['VERSION', []]],), SieveProtocolError),
  257. (([['VERSION', 'foo', 'bar']],), SieveProtocolError),
  258. )
  259. MESSAGES: Final[tuple[Test[str], ...]] = (
  260. # No message
  261. ((Response(Atom('OK')),), 'server says OK'),
  262. ((Response(Atom('NO')),), 'server says NO'),
  263. ((Response(Atom('BYE')),), 'server says BYE'),
  264. # Empty message
  265. ((Response(Atom('OK'), message=''),), 'server says OK'),
  266. ((Response(Atom('NO'), message=''),), 'server says NO'),
  267. ((Response(Atom('BYE'), message=''),), 'server says BYE'),
  268. # Message
  269. ((Response(Atom('OK'), message='foo'),), 'foo'),
  270. ((Response(Atom('NO'), message='foo'),), 'foo'),
  271. ((Response(Atom('BYE'), message='foo'),), 'foo')
  272. )
  273. RESPONSES: Final[tuple[Test[Response], ...]] = (
  274. # Simple cases
  275. (([Atom('OK')],), Response(response=Atom('OK'))),
  276. (([Atom('NO')],), Response(response=Atom('NO'))),
  277. (([Atom('BYE')],), Response(response=Atom('BYE'))),
  278. # Response codes
  279. (([Atom('OK'), []],),
  280. Response(response=Atom('OK'))),
  281. (([Atom('NO'), []],),
  282. Response(response=Atom('NO'))),
  283. (([Atom('BYE'), []],),
  284. Response(response=Atom('BYE'))),
  285. (([Atom('OK'), [Atom('foo')]],),
  286. Response(response=Atom('OK'), code=(Atom('foo'),))),
  287. (([Atom('NO'), [Atom('foo')]],),
  288. Response(response=Atom('NO'), code=(Atom('foo'),))),
  289. (([Atom('BYE'), [Atom('foo')]],),
  290. Response(response=Atom('BYE'), code=(Atom('foo'),))),
  291. (([Atom('OK'), ['foo', 'bar']],),
  292. Response(response=Atom('OK'), code=('foo', 'bar'))),
  293. (([Atom('NO'), ['foo', 'bar']],),
  294. Response(response=Atom('NO'), code=('foo', 'bar'))),
  295. (([Atom('BYE'), ['foo', 'bar']],),
  296. Response(response=Atom('BYE'), code=('foo', 'bar'))),
  297. (([Atom('NO'), [None, True, False, 0, 1, [], 'foo']],),
  298. Response(response=Atom('NO'), code=(None, True, False, 0, 1, [], 'foo'))),
  299. (([Atom('NO'), [[[]]]],),
  300. Response(response=Atom('NO'), code=([[]],))),
  301. (([Atom('NO'), [[[Atom('NIL')]]]],),
  302. Response(response=Atom('NO'), code=([[Atom('NIL')]],))),
  303. # Messages
  304. (([Atom('OK'), 'Success'],),
  305. Response(response=Atom('OK'), message='Success')),
  306. (([Atom('NO'), 'Failure'],),
  307. Response(response=Atom('NO'), message='Failure')),
  308. (([Atom('BYE'), 'Bye'],),
  309. Response(response=Atom('BYE'), message='Bye')),
  310. # Full responses
  311. (([Atom('OK'), [Atom('WARNINGS')], 'foo'],),
  312. Response(Atom('OK'), ('warnings',), 'foo')),
  313. (([Atom('NO'), [Atom('AUTH-TOO-WEAK')], 'Authentication failed'],),
  314. Response(Atom('NO'), ('auth-too-weak',), 'Authentication failed')),
  315. (([Atom('NO'), [Atom('ENCRYPT-NEEDED')], 'Authentication failed'],),
  316. Response(Atom('NO'), ('encrypt-needed',), 'Authentication failed')),
  317. (([Atom('NO'), [Atom('QUOTA')], 'Over quota'],),
  318. Response(Atom('NO'), ('quota',), 'Over quota')),
  319. (([Atom('NO'), [Atom('QUOTA/MAXSCRIPTS')], 'Too many scripts'],),
  320. Response(Atom('NO'), ('quota/maxscripts',), 'Too many scripts')),
  321. (([Atom('NO'), [Atom('QUOTA/MAXSIZE')], 'Script too large'],),
  322. Response(Atom('NO'), ('quota/maxsize',), 'Script too large')),
  323. # Examples from RFC 5804
  324. (
  325. ([
  326. Atom('OK'), [
  327. Atom('SASL'),
  328. 'cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA=='
  329. ]
  330. ],),
  331. Response(
  332. Atom('OK'), (
  333. Atom('SASL'),
  334. 'cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA=='
  335. )
  336. )
  337. ),
  338. (
  339. ([
  340. Atom('OK'),
  341. [Atom('WARNINGS')],
  342. 'line 8: server redirect action limit is 2, '
  343. 'this redirect might be ignored'
  344. ],),
  345. Response(
  346. Atom('OK'),
  347. (Atom('WARNINGS'),),
  348. 'line 8: server redirect action limit is 2, '
  349. 'this redirect might be ignored'
  350. )
  351. ),
  352. (
  353. ([Atom('OK'), 'NOOP completed'],),
  354. Response(Atom('OK'), message='NOOP completed')
  355. ),
  356. (
  357. ([Atom('OK'), [Atom('TAG'), 'STARTTLS-SYNC-42'], 'Done'],),
  358. Response(Atom('OK'), (Atom('TAG'), 'STARTTLS-SYNC-42'), 'Done')
  359. ),
  360. (
  361. ([Atom('NO'), [Atom('QUOTA/MAXSIZE')], 'Quota exceeded'],),
  362. Response(Atom('NO'), (Atom('QUOTA/MAXSIZE'),), 'Quota exceeded')
  363. ),
  364. (
  365. ([Atom('NO'), 'line 2: Syntax error'],),
  366. Response(Atom('NO'), message='line 2: Syntax error')
  367. ),
  368. (
  369. ([Atom('BYE'), 'Too many failed authentication attempts'],),
  370. Response(
  371. Atom('BYE'),
  372. message='Too many failed authentication attempts'
  373. )
  374. ),
  375. # Errors
  376. (([],), SieveProtocolError),
  377. (([None],), SieveProtocolError),
  378. (([[]],), SieveProtocolError),
  379. ((['foo'],), SieveProtocolError),
  380. (([Atom('FOO'), None],), SieveProtocolError),
  381. (([Atom('FOO'), Atom('BAR')],), SieveProtocolError),
  382. (([Atom('FOO'), [Atom('BAR')], None],), SieveProtocolError),
  383. (([Atom('FOO'), 'foo', None],), SieveProtocolError),
  384. (([Atom('FOO'), [Atom('BAR')], 'foo', None],), SieveProtocolError),
  385. (([Atom('FOO'), [Atom('BAR')], 'foo', Atom('FOO')],), SieveProtocolError),
  386. (([Atom('FOO'), [Atom('BAR')], 'foo', []],), SieveProtocolError),
  387. (([Atom('FOO'), [Atom('BAR')], 'foo', 'foo'],), SieveProtocolError)
  388. )
  389. CODES: Final[tuple[Test[bool], ...]] = (
  390. # Edge cases
  391. ((Response(Atom('OK')),), False),
  392. ((Response(Atom('OK')), ''), False),
  393. ((Response(Atom('OK'), ('',)),), False),
  394. ((Response(Atom('OK'), ('',)), ''), True),
  395. ((Response(Atom('OK'), ('',)), 'foo'), False),
  396. ((Response(Atom('OK')), '/'), False),
  397. ((Response(Atom('OK'), ('/',)),), False),
  398. ((Response(Atom('OK'), ('/',)), '/'), True),
  399. ((Response(Atom('OK'), ('/',)), 'foo'), False),
  400. ((Response(Atom('OK'), ('',)), '/'), True),
  401. ((Response(Atom('OK'), ('/',)), ''), True),
  402. ((Response(Atom('OK'), ('foo',)), ''), False),
  403. ((Response(Atom('OK'), ('foo',)), '/'), False),
  404. # Category in tree
  405. ((Response(Atom('OK'), ('foo',)), 'foo'), True),
  406. ((Response(Atom('OK'), ('foo/',)), 'foo'), True),
  407. ((Response(Atom('OK'), ('foo/bar',)), 'foo'), True),
  408. ((Response(Atom('OK'), ('',)), 'Foo'), False),
  409. ((Response(Atom('OK'), ('Foo',)), ''), False),
  410. ((Response(Atom('OK'), ('Foo',)), 'foo'), True),
  411. ((Response(Atom('OK'), ('FoO/',)), 'foo'), True),
  412. ((Response(Atom('OK'), ('foO/BAR',)), 'foo'), True),
  413. ((Response(Atom('OK'), ('',)), 'FOO'), False),
  414. ((Response(Atom('OK'), ('foo',)), ''), False),
  415. ((Response(Atom('OK'), ('foo',)), 'FOO'), True),
  416. ((Response(Atom('OK'), ('foo/',)), 'Foo'), True),
  417. ((Response(Atom('OK'), ('foo/bar',)), 'fOo'), True),
  418. ((Response(Atom('OK'), ('foo',)), 'bar'), False),
  419. ((Response(Atom('OK'), ('foo/',)), 'bar'), False),
  420. ((Response(Atom('OK'), ('foo/bar',)), 'bar'), False),
  421. ((Response(Atom('OK'), ('Foo',)), 'bar'), False),
  422. ((Response(Atom('OK'), ('FoO/',)), 'bar'), False),
  423. ((Response(Atom('OK'), ('foO/BAR',)), 'bar'), False),
  424. ((Response(Atom('OK'), ('foo',)), 'Bar'), False),
  425. ((Response(Atom('OK'), ('foo/',)), 'bAr'), False),
  426. ((Response(Atom('OK'), ('foo/bar',)), 'baR'), False),
  427. # Tree in category
  428. ((Response(Atom('OK'), ('foo',)), ''), False),
  429. ((Response(Atom('OK'), ('foo',)), '/foo'), False),
  430. ((Response(Atom('OK'), ('foo',)), '/foo/'), False),
  431. ((Response(Atom('OK'), ('foo',)), '/foo/bar'), False),
  432. ((Response(Atom('OK'), ('foo',)), 'foo'), True),
  433. ((Response(Atom('OK'), ('foo',)), 'foo/'), True),
  434. ((Response(Atom('OK'), ('foo',)), 'foo/bar'), False),
  435. ((Response(Atom('OK'), ('Foo',)), 'foo'), True),
  436. ((Response(Atom('OK'), ('fOo',)), 'foo/'), True),
  437. ((Response(Atom('OK'), ('foO',)), 'foo/bar'), False),
  438. ((Response(Atom('OK'), ('foo',)), '/FOO'), False),
  439. ((Response(Atom('OK'), ('foo',)), '/foO/'), False),
  440. ((Response(Atom('OK'), ('foo',)), '/foo/BaR'), False),
  441. ((Response(Atom('OK'), ('foo',)), 'Foo'), True),
  442. ((Response(Atom('OK'), ('foo',)), 'fOo/'), True),
  443. ((Response(Atom('OK'), ('foo',)), 'foO/bAR'), False),
  444. ((Response(Atom('OK'), ('foo',)), 'bar/foo'), False),
  445. ((Response(Atom('OK'), ('foo',)), 'bar/foo'), False),
  446. ((Response(Atom('OK'), ('foo',)), 'bar/foo'), False),
  447. ((Response(Atom('OK'), ('Foo',)), 'bar/foo'), False),
  448. ((Response(Atom('OK'), ('FoO',)), 'bar/foo'), False),
  449. ((Response(Atom('OK'), ('foO',)), 'bar/foo'), False),
  450. ((Response(Atom('OK'), ('foo',)), 'Bar/FOO'), False),
  451. ((Response(Atom('OK'), ('foo',)), 'bAr/foO'), False),
  452. ((Response(Atom('OK'), ('foo',)), 'baR/Foo'), False),
  453. # Tree in tree
  454. ((Response(Atom('OK'), ('/bar',)), '/bar'), True),
  455. ((Response(Atom('OK'), ('/bar/',)), '/bar'), True),
  456. ((Response(Atom('OK'), ('/bar',)), '/bar/'), True),
  457. ((Response(Atom('OK'), ('/bar',)), '/foo'), False),
  458. ((Response(Atom('OK'), ('/bar/baz',)), '/bar'), True),
  459. ((Response(Atom('OK'), ('/bar',)), '/bar/baz'), False),
  460. ((Response(Atom('OK'), ('foo/bar',)), 'foo/bar'), True),
  461. ((Response(Atom('OK'), ('foo/bar/',)), 'foo/bar'), True),
  462. ((Response(Atom('OK'), ('foo/bar',)), 'foo/bar/'), True),
  463. ((Response(Atom('OK'), ('foo/bar',)), 'bar/foo'), False),
  464. ((Response(Atom('OK'), ('foo/bar/baz',)), 'foo/bar'), True),
  465. ((Response(Atom('OK'), ('foo/bar',)), 'foo/bar/baz'), False),
  466. # Regular expressions
  467. ((Response(Atom('OK'), ('.',)), '.'), True),
  468. ((Response(Atom('OK'), ('foo',)), '.'), False),
  469. ((Response(Atom('OK'), ('foo',)), '.*'), False),
  470. )
  471. ERRORS: Final[tuple[Test[SieveError], ...]] = (
  472. ((Response(Atom('')),), SieveOperationError(Atom(''))),
  473. ((Response(Atom('FOO')),), SieveOperationError(Atom('FOO'))),
  474. ((Response(Atom('OK')),), SieveOperationError(Atom('OK'))),
  475. ((Response(Atom('NO')),), SieveOperationError()),
  476. ((Response(Atom('BYE')),), SieveConnectionError()),
  477. )
  478. URLS: Final[tuple[Test[URL], ...]] = (
  479. (('foo',),
  480. URL(hostname='foo')),
  481. (('foo:0',),
  482. URL(hostname='foo', port=0)),
  483. (('foo/user',),
  484. URL(hostname='foo', owner='user')),
  485. (('foo/user/script',),
  486. URL(hostname='foo', owner='user', scriptname='script')),
  487. (('foo/user/script/script',),
  488. URL(hostname='foo', owner='user', scriptname='script/script')),
  489. (('sieve://foo',),
  490. URL(hostname='foo')),
  491. (('http://foo',),
  492. URL(scheme='http', hostname='foo')),
  493. (('user@foo',),
  494. URL(username='user', hostname='foo')),
  495. (('user:password@foo',),
  496. URL(username='user', password='password', # nosec B106
  497. hostname='foo')),
  498. (('user:password@foo/jdoe',),
  499. URL(username='user', password='password', # nosec B106
  500. hostname='foo', owner='jdoe')),
  501. (('sieve://user:password@foo/jdoe',),
  502. URL(username='user', password='password', # nosec B106
  503. hostname='foo', owner='jdoe')),
  504. (('sieve://user:password@foo:123/jdoe/script',),
  505. URL(username='user', password='password', # nosec B106
  506. hostname='foo', port=123, owner='jdoe', scriptname='script')),
  507. # Errors
  508. (('',), ValueError),
  509. (('sieve://',), ValueError),
  510. (('sieve://',), ValueError),
  511. ((':80',), ValueError),
  512. (('/foo',), ValueError),
  513. (('sieve://foo?bar=true',), ValueError),
  514. )
  515. REVERSEURLS: Final[tuple[Test[str], ...]] = tuple(
  516. ((u,), s if re.match(r'\w+://', s) else 'sieve://' + s)
  517. for ((s,), u) in URLS
  518. if not (isinstance(u, type) and issubclass(u, Exception)) # type: ignore
  519. )
  520. #
  521. # Tests
  522. #
  523. class TestSieveManager(unittest.TestCase):
  524. test_validname = makerunner(SieveManager.validname, SCRIPTNAMES)
  525. test_validname_chk = makerunner(SieveManager.validname, SCRIPTNAME_ERRORS)
  526. class TestCapabilities(unittest.TestCase):
  527. test_fromlines = makerunner(Capabilities.fromlines, CAPABILITIES)
  528. class TestResponse(unittest.TestCase):
  529. test_str = makerunner(Response.__str__, MESSAGES)
  530. test_fromline = makerunner(Response.fromline, RESPONSES)
  531. test_matches = makerunner(Response.matches, CODES)
  532. test_toerror = makerunner(Response.toerror, ERRORS)
  533. class TestURL(unittest.TestCase):
  534. test_fromurl = makerunner(URL.fromstr, URLS)
  535. test_str = makerunner(URL.__str__, REVERSEURLS)
  536. #
  537. # Boilerplate
  538. #
  539. if __name__ == '__main__':
  540. unittest.main()