1# -*- coding: utf-8 -*-
2# vim:ts=4:sw=4:expandtab
3
4from __future__ import print_function, unicode_literals
5import os
6import re
7import six
8import pytest
9from muacrypt import mime
10from .test_account import gen_ac_mail_msg
11
12
13@pytest.fixture
14def account_maker(mycmd):
15    def account_maker(name, addr):
16        mycmd.run_ok(["add-account", "-a", name, "--email-regex=" + addr])
17        acc = mycmd.get_account(name)
18        acc.addr = addr
19        return acc
20    return account_maker
21
22
23def test_help(cmd):
24    cmd.run_ok([], """
25        *make-header*
26        *export-public-key*
27        *export-secret-key*
28    """)
29    cmd.run_ok(["--help"], """
30        *access and manage*
31    """)
32
33
34def test_init_and_make_header(mycmd):
35    mycmd.run_fail(["make-header", "xyz"], """
36        *AccountNotFound*xyz*
37    """)
38    adr = "x@yz.org"
39    mycmd.run_ok(["add-account", "--email-regex", adr])
40    out = mycmd.run_ok(["make-header", adr])
41    r = mime.parse_one_ac_header_from_string(out)
42    assert "prefer-encrypt" not in out
43    assert "type" not in out
44    assert r.addr == adr
45    out2 = mycmd.run_ok(["make-header", adr])
46    assert out == out2
47
48
49def test_init_and_make_header_with_envvar(cmd, tmpdir):
50    with tmpdir.as_cwd():
51        os.environ["MUACRYPT_BASEDIR"] = "."
52        test_init_and_make_header(cmd)
53
54
55def test_exports_and_status_plain(mycmd):
56    mycmd.run_ok(["add-account", "--email-regex=123@z.org"])
57    out = mycmd.run_ok(["export-public-key"])
58    check_ascii(out)
59    out = mycmd.run_ok(["export-secret-key"])
60    check_ascii(out)
61    out = mycmd.run_ok(["status"], """
62        account-dir:*
63        *account*default*
64        *prefer-encrypt*nopreference*
65        *own-keyhandle:*
66    """)
67    out = mycmd.run_ok(["status", "-v"], """
68        account-dir:*
69        *account*default*
70        *prefer-encrypt*nopreference*
71        *own-keyhandle:*
72    """)
73
74
75def check_ascii(out):
76    if isinstance(out, six.text_type):
77        out.encode("ascii")
78    else:
79        out.decode("ascii")
80
81
82class TestProcessIncoming:
83    def test_process_incoming(self, mycmd, datadir):
84        mycmd.run_ok(["add-account", "-a", "account1", "--email-regex=some@example.org"])
85        mail = datadir.read("rsa2048-simple.eml")
86        mycmd.run_fail(["process-incoming"], """
87            *AccountNotFound*bob@testsuite.autocrypt.org*
88        """, input=mail)
89
90        msg = mime.parse_message_from_string(mail)
91        msg.replace_header("Delivered-To", "some@example.org")
92        newmail = msg.as_string()
93        out = mycmd.run_ok(["process-incoming"], """
94            *processed*account*account1*
95        """, input=newmail)
96
97        # now export the public key
98        m = re.search(r'key=(\w+)', out)
99        keyhandle, = m.groups()
100        mycmd.run_ok(["export-public-key", "--account=account1", keyhandle])
101        mycmd.run_ok(["status"])
102
103    def test_process_incoming_no_autocrypt(self, mycmd):
104        mycmd.run_ok(["add-account", "--email-regex=b@b.org"])
105        mycmd.run_ok(["peerstate", "a@a.org"])
106        msg = mime.gen_mail_msg(From="Alice <a@a.org>", To=["b@b.org"], _dto=True)
107        mycmd.run_ok(["process-incoming"], """
108            *processed*default*no*Autocrypt*header*
109        """, input=msg.as_string())
110        mycmd.run_ok(["peerstate", "a@a.org"])
111
112    def test_peerstate_with_ac_keys(self, mycmd, account_maker):
113        acc1 = account_maker("acc1", "a@a.org")
114        acc2 = account_maker("acc2", "b@b.org")
115        acc2.process_incoming(gen_ac_mail_msg(acc1, acc2))
116        mycmd.run_ok(["peerstate", "-a", "acc2", "a@a.org"])
117        acc1.process_incoming(gen_ac_mail_msg(acc2, acc1))
118        mycmd.run_ok(["peerstate", "-a", "acc1", "b@b.org"])
119
120    def test_twice(self, mycmd, account_maker, linematch):
121        acc1 = account_maker("acc1", "a@a.org")
122        acc2 = account_maker("acc2", "b@b.org")
123        msg = gen_ac_mail_msg(acc1, acc2)
124        mycmd.run_ok(["process-incoming", "-a", "acc2"], input=msg.as_string())
125        out = mycmd.run_ok(["process-incoming", "-a", "acc2"], input=msg.as_string())
126        linematch(out, """
127            *already known*
128        """)
129        out = mycmd.run_ok(["process-incoming", "-a", "acc2", "--reparse"], input=msg.as_string())
130        linematch(out, """
131            *processed*found*
132        """)
133
134
135class TestScandir:
136    def test_scandir_incoming_ac(self, mycmd, account_maker, tmpdir):
137        acc1 = account_maker("account1", "acc1@x.org")
138        acc2 = account_maker("account2", "acc2@x.org")
139
140        maildir = tmpdir.ensure("maildir", dir=True)
141        msg = gen_ac_mail_msg(acc1, acc2, _dto=True)
142        maildir.join("msg1").write(msg.as_string())
143
144        peerstate = acc2.get_peerstate("acc1@x.org")
145        assert not peerstate.has_direct_key()
146        mycmd.run_ok(["scandir-incoming", str(maildir)])
147        peerstate = acc2.get_peerstate("acc1@x.org")
148        assert peerstate.has_direct_key()
149
150    def test_scandir_incoming_ac_twice(self, mycmd, account_maker, tmpdir, linematch):
151        acc1 = account_maker("account1", "acc1@x.org")
152        acc2 = account_maker("account2", "acc2@x.org")
153
154        maildir = tmpdir.ensure("maildir", dir=True)
155        msg = gen_ac_mail_msg(acc1, acc2, _dto=True)
156        maildir.join("msg1").write(msg.as_string())
157        msg2 = gen_ac_mail_msg(acc1, acc2, _dto=True)
158        maildir.join("msg2").write(msg2.as_string())
159        mycmd.run_ok(["scandir-incoming", str(maildir)])
160        peerstate = acc2.get_peerstate("acc1@x.org")
161        assert peerstate.has_direct_key()
162        out = mycmd.run_ok(["scandir-incoming", str(maildir)])
163        linematch(out, """
164            *already known*
165        """)
166        out = mycmd.run_ok(["scandir-incoming", "--reparse", str(maildir)])
167        linematch(out, """
168            *found Autocrypt*
169        """)
170
171
172class TestAccountCommands:
173    def test_add_list_del_account(self, mycmd):
174        mycmd.run_ok(["status"], """
175            *no accounts configured*
176        """)
177        mycmd.run_ok(["add-account", "--email-regex=home@example.org"], """
178            *account added*default*
179        """)
180        mycmd.run_ok(["status"], """
181            *account*default*
182            *home@example.org*
183        """)
184        mycmd.run_ok(["del-account"])
185        mycmd.run_ok(["status"], """
186            *no accounts configured*
187        """)
188
189    def test_add_two_accounts_requires_option(self, mycmd):
190        mycmd.run_ok(["add-account"], """
191            *account added*default*
192        """)
193        mycmd.run_fail(["add-account"], """
194            AccountExists*default*
195        """)
196
197    def test_modify_account_prefer_encrypt(self, mycmd):
198        mycmd.run_ok(["add-account"])
199        mycmd.run_ok(["status"], """
200            *account*default*
201        """)
202        mycmd.run_ok(["mod-account", "--prefer-encrypt=mutual"], """
203            *account modified*default*
204            *email?regex*.**
205            *prefer-encrypt*mutual*
206        """)
207        mycmd.run_ok(["mod-account", "--email-regex=xyz"], """
208            *account modified*default*
209            *email?regex*xyz*
210            *prefer-encrypt*mutual*
211        """)
212
213        mycmd.run_ok(["mod-account", "--prefer-encrypt=nopreference"], """
214            *account modified*default*
215            *email?regex*xyz*
216            *prefer-encrypt*nopreference*
217        """)
218
219    def test_init_existing_key_native_gpg(self, mycmd, monkeypatch, bingpg, gpgpath):
220        adr = "x@y.org"
221        keyhandle = bingpg.gen_secret_key(adr)
222        monkeypatch.setenv("GNUPGHOME", bingpg.homedir)
223        mycmd.run_ok(["add-account", "--use-key", adr,
224                      "--gpgbin=%s" % gpgpath, "--use-system-keyring"], """
225                *gpgmode*system*
226                *gpgbin*{}*
227                *own-keyhandle*{}*
228        """.format(gpgpath, keyhandle))
229        mycmd.run_ok(["make-header", adr], """
230            *Autocrypt*addr=x@y.org*
231        """)
232
233    def test_test_email(self, mycmd):
234        mycmd.run_ok(["add-account", "--email-regex=(home|office)@example.org"])
235        mycmd.run_ok(["find-account", "home@example.org"])
236        mycmd.run_ok(["find-account", "office@example.org"])
237        mycmd.run_fail(["find-account", "xhome@example.org"], """
238            *AccountNotFound*xhome@example.org*
239        """)
240
241
242class TestProcessOutgoing:
243
244    def test_simple(self, mycmd, gen_mail):
245        mycmd.run_ok(["add-account"])
246        mail = gen_mail()
247        out1 = mycmd.run_ok(["process-outgoing"], input=mail.as_string())
248        m = mime.parse_message_from_string(out1)
249        assert len(m.get_all("Autocrypt")) == 1
250        found_header = "Autocrypt: " + m["Autocrypt"]
251        gen_header = mycmd.run_ok(["make-header", "a@a.org"])
252        x1 = mime.parse_one_ac_header_from_string(gen_header)
253        x2 = mime.parse_one_ac_header_from_string(found_header)
254        assert x1 == x2
255
256    def test_matching_account(self, mycmd, gen_mail):
257        mycmd.run_ok(["add-account", "--email-regex=account1@a.org"])
258        mail = gen_mail(From="x@y.org")
259        # mycmd.run_fail(["process-outgoing"], input=mail.as_string(), fnl="""
260        #     *AccountNotFound*x@y.org*
261        # """)
262        out0 = mycmd.run_fail(["process-outgoing"], input=mail.as_string())
263        assert "Autocrypt" not in out0
264
265        mail = gen_mail(From="account1@a.org")
266        out1 = mycmd.run_ok(["process-outgoing"], input=mail.as_string())
267        msg2 = mime.parse_message_from_string(out1)
268        assert "account1@a.org" in msg2["Autocrypt"]
269
270    def test_simple_dont_replace(self, mycmd, gen_mail):
271        mycmd.run_ok(["add-account"])
272        mail = gen_mail()
273        gen_header = mycmd.run_ok(["make-header", "x@x.org"])
274        mail.add_header("Autocrypt", gen_header)
275
276        out1 = mycmd.run_ok(["process-outgoing"], input=mail.as_string())
277        m = mime.parse_message_from_string(out1)
278        assert len(m.get_all("Autocrypt")) == 1
279        x1 = mime.parse_ac_headervalue(m["Autocrypt"])
280        x2 = mime.parse_ac_headervalue(gen_header)
281        assert x1 == x2
282
283    @pytest.mark.parametrize("addr", ["a@a.org", "ño@example.org"])
284    def test_sendmail(self, mycmd, gen_mail, popen_mock, addr):
285        mycmd.run_ok(["add-account"])
286        mail = gen_mail().as_string()
287        pargs = ["-oi", addr]
288        mycmd.run_ok(["sendmail", "-f", "--"] + pargs, input=mail)
289        assert len(popen_mock.calls) == 1
290        call = popen_mock.pop_next_call()
291        for x in pargs:
292            assert x in call.args
293        # make sure unknown option is passed to pipe
294        assert "-f" in call.args
295        out_msg = mime.parse_message_from_string(call.input.decode("utf8"))
296        assert "Autocrypt" in out_msg, out_msg.as_string()
297
298    def test_sendmail_no_account(self, mycmd, gen_mail, popen_mock):
299        mycmd.run_ok(["add-account", "--email-regex=account1@a.org"])
300        mycmd.run_ok(["mod-account", "--email-regex", "123123"])
301        mail = gen_mail().as_string()
302        pargs = ["-oi", "b@b.org"]
303        mycmd.run_fail(["sendmail", "-f", "--"] + pargs, input=mail)
304        # assert len(popen_mock.calls) == 1
305        # call = popen_mock.pop_next_call()
306        # for x in pargs:
307        #     assert x in call.args
308        # # make sure unknown option is passed to pipe
309        # assert "-f" in call.args
310        # out_msg = mime.parse_message_from_string(call.input)
311        # assert "Autocrypt" not in out_msg, out_msg.as_string()
312
313    def test_sendmail_fails(self, mycmd, gen_mail, popen_mock):
314        mycmd.run_ok(["add-account", "--email-regex=.*"])
315        mail = gen_mail().as_string()
316        pargs = ["-oi", "b@b.org"]
317        popen_mock.mock_next_call(ret=2)
318        mycmd.run_fail(["sendmail", "-f", "--", "--qwe"] + pargs, input=mail, code=2)
319        assert len(popen_mock.calls) == 1
320        call = popen_mock.pop_next_call()
321        for x in pargs:
322            assert x in call.args
323        # make sure unknown option is passed to pipe
324        assert "-f" in call.args
325        assert "--qwe" in call.args
326
327    def test_import_keydata(self, mycmd, datadir):
328        mycmd.run_ok(["add-account"])
329        keydata = datadir.read_bytes("test1_autocrypt_org.key")
330        mycmd.run_ok(["import-public-key"], input=keydata)
331        out = mycmd.run_ok(["recommend", "test1@autocrypt.org"])
332        assert "available" in out
333
334        mycmd.run_ok(["mod-account", "--prefer-encrypt", "mutual"])
335        out = mycmd.run_ok(["import-public-key", "--prefer-encrypt=mutual"], input=keydata)
336        assert "imported" in out
337        out = mycmd.run_ok(["recommend", "test1@autocrypt.org"])
338        assert "encrypt" in out
339
340        mycmd.run_ok(["import-public-key", "--prefer-encrypt=nopreference"], input=keydata)
341        out = mycmd.run_ok(["recommend", "test1@autocrypt.org"])
342        assert "available" in out
343
344
345class TestRecommendation:
346    def test_recommend_empty(self, mycmd):
347        mycmd.run_ok(["add-account", "-a", "home"])
348        mycmd.run_ok(["recommend", "-a", "home", "unknown@email.org"])
349
350    def test_recommend_one(self, mycmd):
351        addr1 = "a@a.org"
352        addr2 = "b@b.org"
353        mycmd.run_ok(["add-account", "-a", "ac1", "--email-regex", addr1])
354        mycmd.run_ok(["add-account", "-a", "ac2", "--email-regex", addr2])
355
356        mycmd.send_mail(addr2, [addr1], Date=0)
357        assert "available" == mycmd.parse_recommendation("ac1", [addr2])
358
359        # switch addr2 and addr1 prefer_encrypt to "mutual", send a mail
360        mycmd.run_ok(["mod-account", "-a", "ac1", "--prefer-encrypt", "mutual"])
361        mycmd.run_ok(["mod-account", "-a", "ac2", "--prefer-encrypt", "mutual"])
362        mycmd.send_mail(addr2, [addr1], Date=1)
363        assert "encrypt" == mycmd.parse_recommendation("ac1", [addr2])
364
365        # send a non-ac mail and ask recommend again
366        mycmd.send_mail(addr2, [addr1], ac=False, Date=2)
367        assert "available" == mycmd.parse_recommendation("ac1", [addr2])
368
369    def test_recommend_two(self, mycmd):
370        addrs = []
371        for i in range(1, 4):
372            addr = "%d@x.org" % i
373            mycmd.run_ok(["add-account", "-a", "ac%d" % i, "--email-regex", addr])
374            addrs.append(addr)
375        addr1, addr2, addr3 = addrs
376
377        mycmd.send_mail(addr2, [addr1])
378        mycmd.send_mail(addr3, [addr1])
379        assert "available" == mycmd.parse_recommendation("ac1", [addr2, addr2])
380
381        # switch all accounts to mutual
382        for name in "ac1 ac2 ac2".split():
383            mycmd.run_ok(["mod-account", "-a", name, "--prefer-encrypt", "mutual"])
384        mycmd.send_mail(addr2, [addr1])
385        mycmd.send_mail(addr3, [addr1])
386        assert "encrypt" == mycmd.parse_recommendation("ac1", [addr2, addr2])
387