Compare commits
312 Commits
test-apach
...
postfix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a421be7b67 | ||
|
|
d9ae16a649 | ||
|
|
88ae0949cc | ||
|
|
e4f5e8f455 | ||
|
|
605eeba234 | ||
|
|
e1e7eaa512 | ||
|
|
97f9ab2804 | ||
|
|
8af5564043 | ||
|
|
7d73788a3a | ||
|
|
591125dcb5 | ||
|
|
64feda281a | ||
|
|
e6bd6287de | ||
|
|
ed9e6a51d8 | ||
|
|
febbb4e97c | ||
|
|
7900612f14 | ||
|
|
e852f57a7e | ||
|
|
d0bf706d26 | ||
|
|
875f9c9604 | ||
|
|
2e11236670 | ||
|
|
558ec4d452 | ||
|
|
81a472b29a | ||
|
|
0e0db9ea44 | ||
|
|
5fa405214b | ||
|
|
d744602279 | ||
|
|
f52f345d3c | ||
|
|
3a4b93889e | ||
|
|
52b053b297 | ||
|
|
d784888eef | ||
|
|
7230f346da | ||
|
|
850ccbddd0 | ||
|
|
5c5c34dc11 | ||
|
|
f00949d269 | ||
|
|
c521c3577f | ||
|
|
7e51aa1aaa | ||
|
|
70d7b0f2ca | ||
|
|
1388e161cd | ||
|
|
4eb9bd7d4a | ||
|
|
c1df4cef6b | ||
|
|
114b7226b6 | ||
|
|
a8a07c9b3a | ||
|
|
ad13ca0159 | ||
|
|
00964a0ae3 | ||
|
|
671b6da2ee | ||
|
|
f125cc7b89 | ||
|
|
b042cb933d | ||
|
|
757ade8277 | ||
|
|
f91c20d8f8 | ||
|
|
fa2c126d0e | ||
|
|
df776a87e7 | ||
|
|
c810bd06d3 | ||
|
|
d18dd1fc18 | ||
|
|
503bffeee8 | ||
|
|
a79e87a76c | ||
|
|
270f66d33c | ||
|
|
4e19c19dfc | ||
|
|
b3a4928173 | ||
|
|
e0ad7c0406 | ||
|
|
8cf6953fba | ||
|
|
ed6e13ccd3 | ||
|
|
c6a5a002ad | ||
|
|
e0308ac04c | ||
|
|
26343821a7 | ||
|
|
8f52b1626f | ||
|
|
361fa62f00 | ||
|
|
f2eb6326cf | ||
|
|
3e3952b545 | ||
|
|
3d3932cca3 | ||
|
|
9a3ba574d7 | ||
|
|
b31d37d9dc | ||
|
|
e57da5768d | ||
|
|
c4a303a113 | ||
|
|
7fd4ed2a42 | ||
|
|
4e7440c88f | ||
|
|
78c0e91732 | ||
|
|
a827462177 | ||
|
|
66953435c9 | ||
|
|
dde0bf0821 | ||
|
|
142bc33545 | ||
|
|
a339de80f4 | ||
|
|
d663f7981a | ||
|
|
72637b2cf6 | ||
|
|
d0ea5958f9 | ||
|
|
4805fb4b88 | ||
|
|
11b820c0e4 | ||
|
|
b21b66c0c0 | ||
|
|
218e15c9d4 | ||
|
|
a6c08a2e25 | ||
|
|
68dc678eed | ||
|
|
5f3be9b1cf | ||
|
|
2ae187b1d6 | ||
|
|
b4b5c44750 | ||
|
|
a29a99fb6f | ||
|
|
0f4c5c2305 | ||
|
|
60c6cc5f2a | ||
|
|
00e28592b6 | ||
|
|
b92df1b71c | ||
|
|
baf0d3343a | ||
|
|
cc3896d5d4 | ||
|
|
b94e268f83 | ||
|
|
b9177948d3 | ||
|
|
3b2e9e49be | ||
|
|
0efc02d6ee | ||
|
|
967a1830e6 | ||
|
|
90ffe2aac0 | ||
|
|
b342f40c2b | ||
|
|
83e37acc8b | ||
|
|
d1f3a2deef | ||
|
|
c9813a44d7 | ||
|
|
b98f541b91 | ||
|
|
50a1f6340f | ||
|
|
8c4ff5cb63 | ||
|
|
290f5b8ce7 | ||
|
|
48c5731a6b | ||
|
|
749f758adb | ||
|
|
2e8a8dfed5 | ||
|
|
25d1f6ec75 | ||
|
|
4c4b63437f | ||
|
|
02c7eca6da | ||
|
|
b72dfc0c08 | ||
|
|
7334fc3066 | ||
|
|
4e5740615c | ||
|
|
5a1d031f07 | ||
|
|
4715b2b12c | ||
|
|
5beaae3b65 | ||
|
|
dfd1cceb9b | ||
|
|
192f0f60da | ||
|
|
b395b72d1b | ||
|
|
a2dbf2fe4c | ||
|
|
86fe5ad362 | ||
|
|
89ae874f89 | ||
|
|
2a217189a6 | ||
|
|
481fb8413b | ||
|
|
d97a15861b | ||
|
|
a15fe57225 | ||
|
|
6c5a8423b8 | ||
|
|
a66500ea38 | ||
|
|
49cdfcec06 | ||
|
|
b50a71ff4e | ||
|
|
b37be61807 | ||
|
|
4a3fd19c93 | ||
|
|
66ba0b5276 | ||
|
|
61c2209110 | ||
|
|
694746409f | ||
|
|
1c258c0a2c | ||
|
|
6c4b3c08a7 | ||
|
|
c2a8ce59ae | ||
|
|
5bf4ad1f52 | ||
|
|
ae08dc6bea | ||
|
|
f89051cc2a | ||
|
|
74b22a596e | ||
|
|
e2d95b3719 | ||
|
|
dca274085d | ||
|
|
619e273ae5 | ||
|
|
baa563f359 | ||
|
|
64f2ddfa80 | ||
|
|
a5f23b5314 | ||
|
|
af38c30c9c | ||
|
|
887871833d | ||
|
|
5d07b70269 | ||
|
|
c43602c908 | ||
|
|
4d24eb83a8 | ||
|
|
7edceec8ac | ||
|
|
c6baa82ee4 | ||
|
|
e75bafa439 | ||
|
|
cc83e9ba52 | ||
|
|
5d355044c4 | ||
|
|
1f95ac9640 | ||
|
|
5928fae89e | ||
|
|
0bf2537a55 | ||
|
|
fd1cef3fa0 | ||
|
|
fee9c86233 | ||
|
|
5cc317408c | ||
|
|
2900d5122c | ||
|
|
c7a8d1cb7a | ||
|
|
0ba508ee2d | ||
|
|
1d37e94e17 | ||
|
|
843e156a51 | ||
|
|
ce88098ba6 | ||
|
|
1cde7f9b54 | ||
|
|
e42a222c5d | ||
|
|
87022782fb | ||
|
|
a58c652443 | ||
|
|
0ce1684ba6 | ||
|
|
af1e94be5a | ||
|
|
e88eac65da | ||
|
|
874c59012a | ||
|
|
b2977ad6a9 | ||
|
|
a435036a1e | ||
|
|
3a8e1d7a70 | ||
|
|
1210c04f14 | ||
|
|
6e2b6a0817 | ||
|
|
9a122626b9 | ||
|
|
d0629e62ea | ||
|
|
b8ce13f96a | ||
|
|
6db2858825 | ||
|
|
4d14423a21 | ||
|
|
959e943de8 | ||
|
|
9e42f6ed08 | ||
|
|
bd20b50879 | ||
|
|
28bb0eb6ac | ||
|
|
074fef773b | ||
|
|
3aeb62cf7e | ||
|
|
8f5b8558d2 | ||
|
|
47a5b7e3ba | ||
|
|
cdc8b94823 | ||
|
|
fedf970284 | ||
|
|
965027ce52 | ||
|
|
1cde095dc2 | ||
|
|
39a01190d5 | ||
|
|
9539f21390 | ||
|
|
9abef4c0bd | ||
|
|
146fce3878 | ||
|
|
904dc11b03 | ||
|
|
7c6c3efb0f | ||
|
|
c87b5d6a78 | ||
|
|
9a71b18b85 | ||
|
|
6da5de6b19 | ||
|
|
147f58bdbc | ||
|
|
fe17c873c0 | ||
|
|
613a8f5e88 | ||
|
|
f04e8259a9 | ||
|
|
828c00b758 | ||
|
|
1d47acddfd | ||
|
|
726afb8b95 | ||
|
|
a1d016d031 | ||
|
|
91f6be6b3b | ||
|
|
97cce82e5a | ||
|
|
622fc72dc1 | ||
|
|
42c63cb6dd | ||
|
|
31e320d0a7 | ||
|
|
7f9dadd681 | ||
|
|
ad40618897 | ||
|
|
e0edc1b7ec | ||
|
|
78a55c3823 | ||
|
|
9cafcf1caf | ||
|
|
8c6d28ce95 | ||
|
|
30ba7e9305 | ||
|
|
a6700e3172 | ||
|
|
0bd8134e5f | ||
|
|
ff5810d78f | ||
|
|
21ff3acf93 | ||
|
|
cebc6f9a20 | ||
|
|
749c4e39e0 | ||
|
|
6a1aa8e6b6 | ||
|
|
bdd4d01dc7 | ||
|
|
6a40e1964b | ||
|
|
82ef8b185a | ||
|
|
dd4f9d35ae | ||
|
|
127d49e837 | ||
|
|
a85fad98c0 | ||
|
|
1c3c69aaad | ||
|
|
3de8c2a651 | ||
|
|
803c39e585 | ||
|
|
51980e212f | ||
|
|
3df343495e | ||
|
|
45aeb5b003 | ||
|
|
67ee3b0488 | ||
|
|
2eba47a716 | ||
|
|
51f90ffafb | ||
|
|
8d6b6c358a | ||
|
|
9da4f93ae5 | ||
|
|
3cf61a54b7 | ||
|
|
43d457aa77 | ||
|
|
499f6c2fad | ||
|
|
9cd71642fb | ||
|
|
7a7329fa19 | ||
|
|
9ce047980a | ||
|
|
02abaf57bd | ||
|
|
3d7b53daf1 | ||
|
|
e99abfacfd | ||
|
|
a2ee328bc0 | ||
|
|
34cba3accf | ||
|
|
3712a45399 | ||
|
|
182e9b29e4 | ||
|
|
2540f1f1e8 | ||
|
|
eea1b0d8c5 | ||
|
|
6e1bcfdb2a | ||
|
|
d7e4d93190 | ||
|
|
0c4e332811 | ||
|
|
46ce09d36d | ||
|
|
269f15f9ee | ||
|
|
21e841fd13 | ||
|
|
a03db04ff4 | ||
|
|
d0bcc13059 | ||
|
|
17425e7337 | ||
|
|
7bdb63376c | ||
|
|
bdbc46fc84 | ||
|
|
0d43d2988a | ||
|
|
79924108c7 | ||
|
|
c033905b16 | ||
|
|
839c523048 | ||
|
|
7c81f23a07 | ||
|
|
e534a43d1a | ||
|
|
fcd1a98201 | ||
|
|
dc606eac7d | ||
|
|
372c96d9fd | ||
|
|
5a9f90dc30 | ||
|
|
ce0a6a1814 | ||
|
|
fa5acdf674 | ||
|
|
31db3b7034 | ||
|
|
30938260d4 | ||
|
|
7bd06a4d35 | ||
|
|
ace9d2383d | ||
|
|
ed0c024209 | ||
|
|
714cb17dcb | ||
|
|
6fb51d5422 | ||
|
|
4b5b9f164f | ||
|
|
3d9d5607bd | ||
|
|
aa417eec15 | ||
|
|
844ec79f01 | ||
|
|
f0b9ef2716 | ||
|
|
8857302347 |
190
certbot-postfix/LICENSE.txt
Normal file
190
certbot-postfix/LICENSE.txt
Normal file
@@ -0,0 +1,190 @@
|
||||
Copyright 2017 Electronic Frontier Foundation and others
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
4
certbot-postfix/MANIFEST.in
Normal file
4
certbot-postfix/MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include certbot_postfix/testdata *
|
||||
recursive-include certbot_postfix/docs *
|
||||
10
certbot-postfix/README.rst
Normal file
10
certbot-postfix/README.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
==========================
|
||||
Postfix plugin for Certbot
|
||||
==========================
|
||||
|
||||
To install your certs with this plugin, run:
|
||||
|
||||
``certbot install --installer postfix --cert-path <path to cert> --key-path <path to key> -d <MX hostname>``
|
||||
|
||||
And there you go! If you'd like to obtain these certificates via certbot, there's more documentation on how to do this `here <https://certbot.eff.org/docs/using.html#getting-certificates-and-choosing-plugins>`_.
|
||||
|
||||
3
certbot-postfix/certbot_postfix/__init__.py
Normal file
3
certbot-postfix/certbot_postfix/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Certbot Postfix plugin."""
|
||||
|
||||
from certbot_postfix.installer import Installer
|
||||
63
certbot-postfix/certbot_postfix/constants.py
Normal file
63
certbot-postfix/certbot_postfix/constants.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Postfix plugin constants."""
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Dict, Tuple, Union
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
MINIMUM_VERSION = (2, 11,)
|
||||
|
||||
# If the value of a default VAR is a tuple, then the values which
|
||||
# come LATER in the tuple are more strict/more secure.
|
||||
# Certbot will default to the first value in the tuple, but will
|
||||
# not override "more secure" settings.
|
||||
|
||||
ACCEPTABLE_SERVER_SECURITY_LEVELS = ("may", "encrypt")
|
||||
ACCEPTABLE_CLIENT_SECURITY_LEVELS = ("may", "encrypt",
|
||||
"dane", "dane-only",
|
||||
"fingerprint",
|
||||
"verify", "secure")
|
||||
ACCEPTABLE_CIPHER_LEVELS = ("medium", "high")
|
||||
|
||||
# Exporting certain ciphers to prevent logjam: https://weakdh.org/sysadmin.html
|
||||
EXCLUDE_CIPHERS = ("aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, "
|
||||
"EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA",)
|
||||
|
||||
|
||||
TLS_VERSIONS = ("SSLv2", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2")
|
||||
# Should NOT use SSLv2/3.
|
||||
ACCEPTABLE_TLS_VERSIONS = ("TLSv1", "TLSv1.1", "TLSv1.2")
|
||||
|
||||
# Variables associated with enabling opportunistic TLS.
|
||||
TLS_SERVER_VARS = {
|
||||
"smtpd_tls_security_level": ACCEPTABLE_SERVER_SECURITY_LEVELS,
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
TLS_CLIENT_VARS = {
|
||||
"smtp_tls_security_level": ACCEPTABLE_CLIENT_SECURITY_LEVELS,
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
# Default variables for a secure MTA server [receiver].
|
||||
DEFAULT_SERVER_VARS = {
|
||||
"smtpd_tls_auth_only": ("yes",),
|
||||
"smtpd_tls_mandatory_protocols": ("!SSLv2, !SSLv3",),
|
||||
"smtpd_tls_protocols": ("!SSLv2, !SSLv3",),
|
||||
"smtpd_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
"smtpd_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
"smtpd_tls_exclude_ciphers": EXCLUDE_CIPHERS,
|
||||
"smtpd_tls_eecdh_grade": ("strong",),
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
|
||||
# Default variables for a secure MTA client [sender].
|
||||
DEFAULT_CLIENT_VARS = {
|
||||
"smtp_tls_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
"smtp_tls_exclude_ciphers": EXCLUDE_CIPHERS,
|
||||
"smtp_tls_mandatory_ciphers": ACCEPTABLE_CIPHER_LEVELS,
|
||||
} # type:Dict[str, Tuple[str, ...]]
|
||||
|
||||
CLI_DEFAULTS = dict(
|
||||
config_dir="/etc/postfix",
|
||||
ctl="postfix",
|
||||
config_utility="postconf",
|
||||
tls_only=False,
|
||||
ignore_master_overrides=False,
|
||||
server_only=False,
|
||||
)
|
||||
"""CLI defaults."""
|
||||
288
certbot-postfix/certbot_postfix/installer.py
Normal file
288
certbot-postfix/certbot_postfix/installer.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""certbot installer plugin for postfix."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import zope.interface
|
||||
import zope.component
|
||||
import six
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
from certbot import util as certbot_util
|
||||
from certbot.plugins import common as plugins_common
|
||||
|
||||
from certbot_postfix import constants
|
||||
from certbot_postfix import postconf
|
||||
from certbot_postfix import util
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Callable, Dict, List
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@zope.interface.implementer(interfaces.IInstaller)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class Installer(plugins_common.Installer):
|
||||
"""Certbot installer plugin for Postfix.
|
||||
|
||||
:ivar str config_dir: Postfix configuration directory to modify
|
||||
:ivar list save_notes: documentation for proposed changes. This is
|
||||
cleared and stored in Certbot checkpoints when save() is called
|
||||
|
||||
:ivar postconf: Wrapper for Postfix configuration command-line tool.
|
||||
:type postconf: :class: `certbot_postfix.postconf.ConfigMain`
|
||||
:ivar postfix: Wrapper for Postfix command-line tool.
|
||||
:type postfix: :class: `certbot_postfix.util.PostfixUtil`
|
||||
"""
|
||||
|
||||
description = "Configure TLS with the Postfix MTA"
|
||||
|
||||
@classmethod
|
||||
def add_parser_arguments(cls, add):
|
||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
|
||||
help="Path to the 'postfix' control program.")
|
||||
# This directory points to Postfix's configuration directory.
|
||||
add("config-dir", default=constants.CLI_DEFAULTS["config_dir"],
|
||||
help="Path to the directory containing the "
|
||||
"Postfix main.cf file to modify instead of using the "
|
||||
"default configuration paths.")
|
||||
add("config-utility", default=constants.CLI_DEFAULTS["config_utility"],
|
||||
help="Path to the 'postconf' executable.")
|
||||
add("tls-only", action="store_true", default=constants.CLI_DEFAULTS["tls_only"],
|
||||
help="Only set params to enable opportunistic TLS and install certificates.")
|
||||
add("server-only", action="store_true", default=constants.CLI_DEFAULTS["server_only"],
|
||||
help="Only set server params (prefixed with smtpd*)")
|
||||
add("ignore-master-overrides", action="store_true",
|
||||
default=constants.CLI_DEFAULTS["ignore_master_overrides"],
|
||||
help="Ignore errors reporting overridden TLS parameters in master.cf.")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Installer, self).__init__(*args, **kwargs)
|
||||
# Wrapper around postconf commands
|
||||
self.postfix = None
|
||||
self.postconf = None
|
||||
|
||||
# Files to save
|
||||
self.save_notes = [] # type: List[str]
|
||||
|
||||
self._enhance_func = {} # type: Dict[str, Callable[[str, str], None]]
|
||||
# Since we only need to enable TLS once for all domains,
|
||||
# keep track of whether this enhancement was already called.
|
||||
self._tls_enabled = False
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare the installer.
|
||||
|
||||
:raises errors.PluginError: when an unexpected error occurs
|
||||
:raises errors.MisconfigurationError: when the config is invalid
|
||||
:raises errors.NoInstallationError: when can't find installation
|
||||
:raises errors.NotSupportedError: when version is not supported
|
||||
"""
|
||||
# Verify postfix and postconf are installed
|
||||
for param in ("ctl", "config_utility",):
|
||||
util.verify_exe_exists(self.conf(param),
|
||||
"Cannot find executable '{0}'. You can provide the "
|
||||
"path to this command with --{1}".format(
|
||||
self.conf(param),
|
||||
self.option_name(param)))
|
||||
|
||||
# Set up CLI tools
|
||||
self.postfix = util.PostfixUtil(self.conf('config-dir'))
|
||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
||||
self.conf('ignore-master-overrides'),
|
||||
self.conf('config-dir'))
|
||||
|
||||
# Ensure current configuration is valid.
|
||||
self.config_test()
|
||||
|
||||
# Check Postfix version
|
||||
self._check_version()
|
||||
self._lock_config_dir()
|
||||
self.install_ssl_dhparams()
|
||||
|
||||
def config_test(self):
|
||||
"""Test to see that the current Postfix configuration is valid.
|
||||
|
||||
:raises errors.MisconfigurationError: If the configuration is invalid.
|
||||
"""
|
||||
self.postfix.test()
|
||||
|
||||
def _check_version(self):
|
||||
"""Verifies that the installed Postfix version is supported.
|
||||
|
||||
:raises errors.NotSupportedError: if the version is unsupported
|
||||
"""
|
||||
if self._get_version() < constants.MINIMUM_VERSION:
|
||||
version_string = '.'.join([str(n) for n in constants.MINIMUM_VERSION])
|
||||
raise errors.NotSupportedError('Postfix version must be at least %s' % version_string)
|
||||
|
||||
def _lock_config_dir(self):
|
||||
"""Stop two Postfix plugins from modifying the config at once.
|
||||
|
||||
:raises .PluginError: if unable to acquire the lock
|
||||
"""
|
||||
try:
|
||||
certbot_util.lock_dir_until_exit(self.conf('config-dir'))
|
||||
except (OSError, errors.LockError):
|
||||
logger.debug("Encountered error:", exc_info=True)
|
||||
raise errors.PluginError(
|
||||
"Unable to lock %s" % self.conf('config-dir'))
|
||||
|
||||
def more_info(self):
|
||||
"""Human-readable string to help the user. Describes steps taken and any relevant
|
||||
info to help the user decide which plugin to use.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
return (
|
||||
"Configures Postfix to try to authenticate mail servers, use "
|
||||
"installed certificates and disable weak ciphers and protocols.{0}"
|
||||
"Server root: {root}{0}"
|
||||
"Version: {version}".format(
|
||||
os.linesep,
|
||||
root=self.conf('config-dir'),
|
||||
version='.'.join([str(i) for i in self._get_version()]))
|
||||
)
|
||||
|
||||
def _get_version(self):
|
||||
"""Return the version of Postfix, as a tuple. (e.g. '2.11.3' is (2, 11, 3))
|
||||
|
||||
:returns: version
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.PluginError: Unable to find Postfix version.
|
||||
"""
|
||||
mail_version = self.postconf.get_default("mail_version")
|
||||
return tuple(int(i) for i in mail_version.split('.'))
|
||||
|
||||
def get_all_names(self):
|
||||
"""Returns all names that may be authenticated.
|
||||
|
||||
:rtype: `set` of `str`
|
||||
|
||||
"""
|
||||
return certbot_util.get_filtered_names(self.postconf.get(var)
|
||||
for var in ('mydomain', 'myhostname', 'myorigin',))
|
||||
|
||||
def _set_vars(self, var_dict):
|
||||
"""Sets all parameters in var_dict to config file. If current value is already set
|
||||
as more secure (acceptable), then don't set/overwrite it.
|
||||
"""
|
||||
for param, acceptable in six.iteritems(var_dict):
|
||||
if not util.is_acceptable_value(param, self.postconf.get(param), acceptable):
|
||||
self.postconf.set(param, acceptable[0], acceptable)
|
||||
|
||||
def _confirm_changes(self):
|
||||
"""Confirming outstanding updates for configuration parameters.
|
||||
|
||||
:raises errors.PluginError: when user rejects the configuration changes.
|
||||
"""
|
||||
updates = self.postconf.get_changes()
|
||||
output_string = "Postfix TLS configuration parameters to update in main.cf:\n"
|
||||
for name, value in six.iteritems(updates):
|
||||
output_string += "{0} = {1}\n".format(name, value)
|
||||
output_string += "Is this okay?\n"
|
||||
if not zope.component.getUtility(interfaces.IDisplay).yesno(output_string,
|
||||
force_interactive=True, default=True):
|
||||
raise errors.PluginError(
|
||||
"Manually rejected configuration changes.\n"
|
||||
"Try using --tls-only or --server-only to change a particular"
|
||||
"subset of configuration parameters.")
|
||||
|
||||
def deploy_cert(self, domain, cert_path,
|
||||
key_path, chain_path, fullchain_path):
|
||||
"""Configure the Postfix SMTP server to use the given TLS cert.
|
||||
|
||||
:param str domain: domain to deploy certificate file
|
||||
:param str cert_path: absolute path to the certificate file
|
||||
:param str key_path: absolute path to the private key file
|
||||
:param str chain_path: absolute path to the certificate chain file
|
||||
:param str fullchain_path: absolute path to the certificate fullchain
|
||||
file (cert plus chain)
|
||||
|
||||
:raises .PluginError: when cert cannot be deployed
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
if self._tls_enabled:
|
||||
return
|
||||
self._tls_enabled = True
|
||||
self.save_notes.append("Configuring TLS for {0}".format(domain))
|
||||
self.postconf.set("smtpd_tls_cert_file", cert_path)
|
||||
self.postconf.set("smtpd_tls_key_file", key_path)
|
||||
self._set_vars(constants.TLS_SERVER_VARS)
|
||||
if not self.conf('server_only'):
|
||||
self._set_vars(constants.TLS_CLIENT_VARS)
|
||||
if not self.conf('tls_only'):
|
||||
self._set_vars(constants.DEFAULT_SERVER_VARS)
|
||||
if not self.conf('server_only'):
|
||||
self._set_vars(constants.DEFAULT_CLIENT_VARS)
|
||||
# Despite the name, this option also supports 2048-bit DH params.
|
||||
# http://www.postfix.org/FORWARD_SECRECY_README.html#server_fs
|
||||
self.postconf.set("smtpd_tls_dh1024_param_file", self.ssl_dhparams)
|
||||
self._confirm_changes()
|
||||
|
||||
def enhance(self, domain, enhancement, options=None):
|
||||
"""Raises an exception since this installer doesn't support any enhancements.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
raise errors.PluginError(
|
||||
"Unsupported enhancement: {0}".format(enhancement))
|
||||
|
||||
def supported_enhancements(self):
|
||||
"""Returns a list of supported enhancements.
|
||||
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
return []
|
||||
|
||||
def save(self, title=None, temporary=False):
|
||||
"""Creates backups and writes changes to configuration files.
|
||||
|
||||
:param str title: The title of the save. If a title is given, the
|
||||
configuration will be saved as a new checkpoint and put in a
|
||||
timestamped directory. `title` has no effect if temporary is true.
|
||||
|
||||
:param bool temporary: Indicates whether the changes made will
|
||||
be quickly reversed in the future (challenges)
|
||||
|
||||
:raises errors.PluginError: when save is unsuccessful
|
||||
"""
|
||||
save_files = set((os.path.join(self.conf('config-dir'), "main.cf"),))
|
||||
self.add_to_checkpoint(save_files,
|
||||
"\n".join(self.save_notes), temporary)
|
||||
self.postconf.flush()
|
||||
|
||||
del self.save_notes[:]
|
||||
|
||||
if title and not temporary:
|
||||
self.finalize_checkpoint(title)
|
||||
|
||||
def recovery_routine(self):
|
||||
super(Installer, self).recovery_routine()
|
||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
||||
self.conf('ignore-master-overrides'),
|
||||
self.conf('config-dir'))
|
||||
|
||||
def rollback_checkpoints(self, rollback=1):
|
||||
"""Rollback saved checkpoints.
|
||||
|
||||
:param int rollback: Number of checkpoints to revert
|
||||
|
||||
:raises .errors.PluginError: If there is a problem with the input or
|
||||
the function is unable to correctly revert the configuration
|
||||
"""
|
||||
super(Installer, self).rollback_checkpoints(rollback)
|
||||
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
||||
self.conf('ignore-master-overrides'),
|
||||
self.conf('config-dir'))
|
||||
|
||||
def restart(self):
|
||||
"""Restart or refresh the server content.
|
||||
|
||||
:raises .PluginError: when server cannot be restarted
|
||||
"""
|
||||
self.postfix.restart()
|
||||
|
||||
152
certbot-postfix/certbot_postfix/postconf.py
Normal file
152
certbot-postfix/certbot_postfix/postconf.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Classes that wrap the postconf command line utility.
|
||||
"""
|
||||
import six
|
||||
from certbot import errors
|
||||
from certbot_postfix import util
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Dict, List, Tuple
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
class ConfigMain(util.PostfixUtilBase):
|
||||
"""A parser for Postfix's main.cf file."""
|
||||
|
||||
def __init__(self, executable, ignore_master_overrides=False, config_dir=None):
|
||||
super(ConfigMain, self).__init__(executable, config_dir)
|
||||
# Whether to ignore overrides from master.
|
||||
self._ignore_master_overrides = ignore_master_overrides
|
||||
# List of all current Postfix parameters, from `postconf` command.
|
||||
self._db = {} # type: Dict[str, str]
|
||||
# List of current master.cf overrides from Postfix config. Dictionary
|
||||
# of parameter name => list of tuples (service name, paramter value)
|
||||
# Note: We should never modify master without explicit permission.
|
||||
self._master_db = {} # type: Dict[str, List[Tuple[str, str]]]
|
||||
# List of all changes requested to the Postfix parameters as they are now
|
||||
# in _db. These changes are flushed to `postconf` on `flush`.
|
||||
self._updated = {} # type: Dict[str, str]
|
||||
self._read_from_conf()
|
||||
|
||||
def _read_from_conf(self):
|
||||
"""Reads initial parameter state from `main.cf` into this object.
|
||||
"""
|
||||
out = self._get_output()
|
||||
for name, value in _parse_main_output(out):
|
||||
self._db[name] = value
|
||||
out = self._get_output_master()
|
||||
for name, value in _parse_main_output(out):
|
||||
service, param_name = name.rsplit("/", 1)
|
||||
if param_name not in self._master_db:
|
||||
self._master_db[param_name] = []
|
||||
self._master_db[param_name].append((service, value))
|
||||
|
||||
def _get_output_master(self):
|
||||
"""Retrieves output for `master.cf` parameters."""
|
||||
return self._get_output('-P')
|
||||
|
||||
def get_default(self, name):
|
||||
"""Retrieves default value of parameter `name` from postfix parameters.
|
||||
|
||||
:param str name: The name of the parameter to fetch.
|
||||
:returns: The default value of parameter `name`.
|
||||
:rtype: str
|
||||
"""
|
||||
out = self._get_output(['-d', name])
|
||||
_, value = next(_parse_main_output(out), (None, None))
|
||||
return value
|
||||
|
||||
def get(self, name):
|
||||
"""Retrieves working value of parameter `name` from postfix parameters.
|
||||
|
||||
:param str name: The name of the parameter to fetch.
|
||||
:returns: The value of parameter `name`.
|
||||
:rtype: str
|
||||
"""
|
||||
if name in self._updated:
|
||||
return self._updated[name]
|
||||
return self._db[name]
|
||||
|
||||
def get_master_overrides(self, name):
|
||||
"""Retrieves list of overrides for parameter `name` in postfix's Master config
|
||||
file.
|
||||
|
||||
:returns: List of tuples (service, value), meaning that parameter `name`
|
||||
is overridden as `value` for `service`.
|
||||
:rtype: `list` of `tuple` of `str`
|
||||
"""
|
||||
if name in self._master_db:
|
||||
return self._master_db[name]
|
||||
return None
|
||||
|
||||
def set(self, name, value, acceptable_overrides=None):
|
||||
"""Sets parameter `name` to `value`. If `name` is overridden by a particular service in
|
||||
`master.cf`, reports any of these parameter conflicts as long as
|
||||
`ignore_master_overrides` was not set.
|
||||
|
||||
.. note:: that this function does not flush these parameter values to main.cf;
|
||||
To do that, use `flush`.
|
||||
|
||||
:param str name: The name of the parameter to set.
|
||||
:param str value: The value of the parameter.
|
||||
:param tuple acceptable_overrides: If the master configuration file overrides `value`
|
||||
with a value in acceptable_overrides.
|
||||
"""
|
||||
if name not in self._db:
|
||||
raise KeyError("Parameter name %s is not a valid Postfix parameter name.", name)
|
||||
# Check to see if this parameter is overridden by master.
|
||||
overrides = self.get_master_overrides(name)
|
||||
if not self._ignore_master_overrides and overrides is not None:
|
||||
util.report_master_overrides(name, overrides, acceptable_overrides)
|
||||
if value != self._db[name]:
|
||||
# _db contains the "original" state of parameters. We only care about
|
||||
# writes if they cause a delta from the original state.
|
||||
self._updated[name] = value
|
||||
elif name in self._updated:
|
||||
# If this write reverts a previously updated parameter back to the
|
||||
# original DB's state, we don't have to keep track of it in _updated.
|
||||
del self._updated[name]
|
||||
|
||||
def flush(self):
|
||||
"""Flushes all parameter changes made using `self.set`, to `main.cf`
|
||||
|
||||
:raises error.PluginError: When flush to main.cf fails for some reason.
|
||||
"""
|
||||
if len(self._updated) == 0:
|
||||
return
|
||||
args = ['-e']
|
||||
for name, value in six.iteritems(self._updated):
|
||||
args.append('{0}={1}'.format(name, value))
|
||||
try:
|
||||
self._get_output(args)
|
||||
except IOError as e:
|
||||
raise errors.PluginError("Unable to save to Postfix config: %v", e)
|
||||
for name, value in six.iteritems(self._updated):
|
||||
self._db[name] = value
|
||||
self._updated = {}
|
||||
|
||||
def get_changes(self):
|
||||
""" Return queued changes to main.cf.
|
||||
|
||||
:rtype: dict[str, str]
|
||||
"""
|
||||
return self._updated
|
||||
|
||||
def _parse_main_output(output):
|
||||
"""Parses the raw output from Postconf about main.cf.
|
||||
|
||||
Expects the output to look like:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
name1 = value1
|
||||
name2 = value2
|
||||
|
||||
:param str output: data postconf wrote to stdout about main.cf
|
||||
|
||||
:returns: generator providing key-value pairs from main.cf
|
||||
:rtype: Iterator[tuple(str, str)]
|
||||
"""
|
||||
for line in output.splitlines():
|
||||
name, _, value = line.partition(" =")
|
||||
yield name, value.strip()
|
||||
|
||||
|
||||
1
certbot-postfix/certbot_postfix/tests/__init__.py
Normal file
1
certbot-postfix/certbot_postfix/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
""" Certbot Postfix Tests """
|
||||
314
certbot-postfix/certbot_postfix/tests/installer_test.py
Normal file
314
certbot-postfix/certbot_postfix/tests/installer_test.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""Tests for certbot_postfix.installer."""
|
||||
from contextlib import contextmanager
|
||||
import copy
|
||||
import functools
|
||||
import os
|
||||
import pkg_resources
|
||||
import six
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
from certbot.tests import util as certbot_test_util
|
||||
|
||||
# pylint: disable=unused-import, no-name-in-module
|
||||
from acme.magic_typing import Dict, Tuple, Union
|
||||
# pylint: enable=unused-import, no-name-in-module
|
||||
|
||||
DEFAULT_MAIN_CF = {
|
||||
"smtpd_tls_cert_file": "",
|
||||
"smtpd_tls_key_file": "",
|
||||
"smtpd_tls_dh1024_param_file": "",
|
||||
"smtpd_tls_security_level": "none",
|
||||
"smtpd_tls_auth_only": "",
|
||||
"smtpd_tls_mandatory_protocols": "",
|
||||
"smtpd_tls_protocols": "",
|
||||
"smtpd_tls_ciphers": "",
|
||||
"smtpd_tls_exclude_ciphers": "",
|
||||
"smtpd_tls_mandatory_ciphers": "",
|
||||
"smtpd_tls_eecdh_grade": "medium",
|
||||
"smtp_tls_security_level": "",
|
||||
"smtp_tls_ciphers": "",
|
||||
"smtp_tls_exclude_ciphers": "",
|
||||
"smtp_tls_mandatory_ciphers": "",
|
||||
"mail_version": "3.2.3"
|
||||
}
|
||||
|
||||
def _main_cf_with(obj):
|
||||
main_cf = copy.copy(DEFAULT_MAIN_CF)
|
||||
main_cf.update(obj)
|
||||
return main_cf
|
||||
|
||||
class InstallerTest(certbot_test_util.ConfigTestCase):
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def setUp(self):
|
||||
super(InstallerTest, self).setUp()
|
||||
_config_file = pkg_resources.resource_filename("certbot_postfix.tests",
|
||||
os.path.join("testdata", "config.json"))
|
||||
self.config.postfix_ctl = "postfix"
|
||||
self.config.postfix_config_dir = self.tempdir
|
||||
self.config.postfix_config_utility = "postconf"
|
||||
self.config.postfix_tls_only = False
|
||||
self.config.postfix_server_only = False
|
||||
self.config.config_dir = self.tempdir
|
||||
|
||||
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
|
||||
def test_set_vars(self, mock_is_acceptable_value):
|
||||
mock_is_acceptable_value.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
mock_is_acceptable_value.return_value = False
|
||||
|
||||
@mock.patch("certbot_postfix.installer.util.is_acceptable_value")
|
||||
def test_acceptable_value(self, mock_is_acceptable_value):
|
||||
mock_is_acceptable_value.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
mock_is_acceptable_value.return_value = False
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_confirm_changes_no_raises_error(self, mock_util):
|
||||
mock_util().yesno.return_value = False
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
self.assertRaises(errors.PluginError, installer.deploy_cert,
|
||||
"example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_save(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.postconf.flush = mock.Mock()
|
||||
installer.reverter = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.save()
|
||||
self.assertEqual(installer.save_notes, [])
|
||||
self.assertEqual(installer.postconf.flush.call_count, 1)
|
||||
self.assertEqual(installer.reverter.add_to_checkpoint.call_count, 1)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_save_with_title(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.postconf.flush = mock.Mock()
|
||||
installer.reverter = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.save(title="new_file!")
|
||||
self.assertEqual(installer.reverter.finalize_checkpoint.call_count, 1)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_rollback_checkpoints_resets_postconf(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.rollback_checkpoints()
|
||||
self.assertEqual(installer.postconf.get_changes(), {})
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_recovery_routine_resets_postconf(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
installer.recovery_routine()
|
||||
self.assertEqual(installer.postconf.get_changes(), {})
|
||||
|
||||
def test_restart(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.restart()
|
||||
self.assertEqual(installer.postfix.restart.call_count, 1)
|
||||
|
||||
def test_add_parser_arguments(self):
|
||||
options = set(("ctl", "config-dir", "config-utility",
|
||||
"tls-only", "server-only", "ignore-master-overrides"))
|
||||
mock_add = mock.MagicMock()
|
||||
|
||||
from certbot_postfix import installer
|
||||
installer.Installer.add_parser_arguments(mock_add)
|
||||
|
||||
for call in mock_add.call_args_list:
|
||||
self.assertTrue(call[0][0] in options)
|
||||
|
||||
def test_no_postconf_prepare(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer_path = "certbot_postfix.installer"
|
||||
exe_exists_path = installer_path + ".certbot_util.exe_exists"
|
||||
path_surgery_path = "certbot_postfix.util.plugins_util.path_surgery"
|
||||
with mock.patch(path_surgery_path, return_value=False):
|
||||
with mock.patch(exe_exists_path, return_value=False):
|
||||
self.assertRaises(errors.NoInstallationError,
|
||||
installer.prepare)
|
||||
|
||||
def test_old_version(self):
|
||||
with create_installer(self.config, main_cf=_main_cf_with({"mail_version": "0.0.1"}))\
|
||||
as installer:
|
||||
self.assertRaises(errors.NotSupportedError, installer.prepare)
|
||||
|
||||
def test_lock_error(self):
|
||||
with create_installer(self.config) as installer:
|
||||
assert_raises = functools.partial(self.assertRaises,
|
||||
errors.PluginError,
|
||||
installer.prepare)
|
||||
certbot_test_util.lock_and_call(assert_raises, self.tempdir)
|
||||
|
||||
|
||||
@mock.patch('certbot.util.lock_dir_until_exit')
|
||||
def test_dir_locked(self, lock_dir):
|
||||
with create_installer(self.config) as installer:
|
||||
lock_dir.side_effect = errors.LockError
|
||||
self.assertRaises(errors.PluginError, installer.prepare)
|
||||
|
||||
def test_more_info(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
output = installer.more_info()
|
||||
self.assertTrue("Postfix" in output)
|
||||
self.assertTrue(self.tempdir in output)
|
||||
self.assertTrue(DEFAULT_MAIN_CF["mail_version"] in output)
|
||||
|
||||
def test_get_all_names(self):
|
||||
config = {"mydomain": "example.org",
|
||||
"myhostname": "mail.example.org",
|
||||
"myorigin": "example.org"}
|
||||
with create_installer(self.config, main_cf=_main_cf_with(config)) as installer:
|
||||
installer.prepare()
|
||||
result = installer.get_all_names()
|
||||
self.assertEqual(result, set(config.values()))
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_deploy(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
from certbot_postfix import constants
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
|
||||
# pylint: disable=protected-access
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
changes = installer.postconf.get_changes()
|
||||
expected = {} # type: Dict[str, Tuple[str, ...]]
|
||||
expected.update(constants.TLS_SERVER_VARS)
|
||||
expected.update(constants.DEFAULT_SERVER_VARS)
|
||||
expected.update(constants.DEFAULT_CLIENT_VARS)
|
||||
self.assertEqual(changes["smtpd_tls_key_file"], "key_path")
|
||||
self.assertEqual(changes["smtpd_tls_cert_file"], "cert_path")
|
||||
for name, value in six.iteritems(expected):
|
||||
self.assertEqual(changes[name], value[0])
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_tls_only(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.conf = lambda x: x == "tls_only"
|
||||
installer.postconf.set = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(installer.postconf.set.call_count, 4)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_server_only(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.conf = lambda x: x == "server_only"
|
||||
installer.postconf.set = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(installer.postconf.set.call_count, 11)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_tls_and_server_only(self, mock_util):
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
installer.conf = lambda x: True
|
||||
installer.postconf.set = mock.Mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(installer.postconf.set.call_count, 3)
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_deploy_twice(self, mock_util):
|
||||
# Deploying twice on the same installer shouldn't do anything!
|
||||
mock_util().yesno.return_value = True
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
with mock.patch.object(ConfigMain, "set", wraps=installer.postconf.set) as fake_set:
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
self.assertEqual(fake_set.call_count, 15)
|
||||
fake_set.reset_mock()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
fake_set.assert_not_called()
|
||||
|
||||
@certbot_test_util.patch_get_utility()
|
||||
def test_deploy_already_secure(self, mock_util):
|
||||
# Should not overwrite "more-secure" parameters
|
||||
mock_util().yesno.return_value = True
|
||||
more_secure = {
|
||||
"smtpd_tls_security_level": "encrypt",
|
||||
"smtpd_tls_protocols": "!SSLv3, !SSLv2, !TLSv1",
|
||||
"smtpd_tls_eecdh_grade": "strong"
|
||||
}
|
||||
with create_installer(self.config,\
|
||||
main_cf=_main_cf_with(more_secure)) as installer:
|
||||
installer.prepare()
|
||||
installer.deploy_cert("example.com", "cert_path", "key_path",
|
||||
"chain_path", "fullchain_path")
|
||||
for param in more_secure.keys():
|
||||
self.assertFalse(param in installer.postconf.get_changes())
|
||||
|
||||
def test_enhance(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
self.assertRaises(errors.PluginError,
|
||||
installer.enhance,
|
||||
"example.org", "redirect")
|
||||
|
||||
def test_supported_enhancements(self):
|
||||
with create_installer(self.config) as installer:
|
||||
installer.prepare()
|
||||
self.assertEqual(installer.supported_enhancements(), [])
|
||||
|
||||
@contextmanager
|
||||
def create_installer(config, main_cf=DEFAULT_MAIN_CF):
|
||||
# pylint: disable=dangerous-default-value
|
||||
"""Creates a Postfix installer with calls to `postconf` and `postfix` mocked out.
|
||||
|
||||
In particular, creates a ConfigMain object that does regular things, but seeds it
|
||||
with values from `main_cf` and `master_cf` dicts.
|
||||
"""
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
from certbot_postfix import installer
|
||||
def _mock_init_postconf(postconf, executable, ignore_master_overrides=False, config_dir=None):
|
||||
# pylint: disable=protected-access,unused-argument
|
||||
postconf._ignore_master_overrides = ignore_master_overrides
|
||||
postconf._db = main_cf
|
||||
postconf._master_db = {}
|
||||
postconf._updated = {}
|
||||
# override get_default to get from main
|
||||
postconf.get_default = lambda name: main_cf[name]
|
||||
with mock.patch.object(ConfigMain, "__init__", _mock_init_postconf):
|
||||
exe_exists_path = "certbot_postfix.installer.certbot_util.exe_exists"
|
||||
with mock.patch(exe_exists_path, return_value=True):
|
||||
with mock.patch("certbot_postfix.installer.util.PostfixUtil",
|
||||
return_value=mock.Mock()):
|
||||
yield installer.Installer(config, "postfix")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
107
certbot-postfix/certbot_postfix/tests/postconf_test.py
Normal file
107
certbot-postfix/certbot_postfix/tests/postconf_test.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Tests for certbot_postfix.postconf."""
|
||||
|
||||
import mock
|
||||
import unittest
|
||||
|
||||
from certbot import errors
|
||||
|
||||
class PostConfTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.PostConf."""
|
||||
def setUp(self):
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
super(PostConfTest, self).setUp()
|
||||
with mock.patch('certbot_postfix.util.PostfixUtilBase._get_output') as mock_call:
|
||||
with mock.patch('certbot_postfix.postconf.ConfigMain._get_output_master') as \
|
||||
mock_master_call:
|
||||
with mock.patch('certbot_postfix.postconf.util.verify_exe_exists') as verify_exe:
|
||||
verify_exe.return_value = True
|
||||
mock_call.return_value = ('default_parameter = value\n'
|
||||
'extra_param =\n'
|
||||
'overridden_by_master = default\n')
|
||||
mock_master_call.return_value = (
|
||||
'service/type/overridden_by_master = master_value\n'
|
||||
'service2/type/overridden_by_master = master_value2\n'
|
||||
)
|
||||
self.config = ConfigMain('postconf', False)
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
@mock.patch('certbot_postfix.postconf.util.verify_exe_exists')
|
||||
def test_get_output_master(self, mock_verify_exe, mock_get_output):
|
||||
from certbot_postfix.postconf import ConfigMain
|
||||
mock_verify_exe.return_value = True
|
||||
ConfigMain('postconf', lambda x, y, z: None)
|
||||
mock_get_output.assert_called_with('-P')
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_read_default(self, mock_get_output):
|
||||
mock_get_output.return_value = 'param = default_value'
|
||||
self.assertEqual(self.config.get_default('param'), 'default_value')
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
||||
def test_set(self, mock_call):
|
||||
self.config.set('extra_param', 'other_value')
|
||||
self.assertEqual(self.config.get('extra_param'), 'other_value')
|
||||
self.config.flush()
|
||||
mock_call.assert_called_with(['-e', 'extra_param=other_value'])
|
||||
|
||||
def test_set_bad_param_name(self):
|
||||
self.assertRaises(KeyError, self.config.set, 'nonexistent_param', 'some_value')
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
||||
def test_write_revert(self, mock_call):
|
||||
self.config.set('default_parameter', 'fake_news')
|
||||
# revert config set
|
||||
self.config.set('default_parameter', 'value')
|
||||
self.config.flush()
|
||||
mock_call.assert_not_called()
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._call')
|
||||
def test_write_default(self, mock_call):
|
||||
self.config.set('default_parameter', 'value')
|
||||
self.config.flush()
|
||||
mock_call.assert_not_called()
|
||||
|
||||
def test_master_overrides(self):
|
||||
self.assertEqual(self.config.get_master_overrides('overridden_by_master'),
|
||||
[('service/type', 'master_value'),
|
||||
('service2/type', 'master_value2')])
|
||||
|
||||
def test_set_check_override(self):
|
||||
self.assertRaises(errors.PluginError, self.config.set,
|
||||
'overridden_by_master', 'new_value')
|
||||
|
||||
def test_ignore_check_override(self):
|
||||
# pylint: disable=protected-access
|
||||
self.config._ignore_master_overrides = True
|
||||
self.config.set('overridden_by_master', 'new_value')
|
||||
|
||||
def test_check_acceptable_overrides(self):
|
||||
self.config.set('overridden_by_master', 'new_value',
|
||||
('master_value', 'master_value2'))
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_flush(self, mock_out):
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
self.config.set('extra_param', 'another_value')
|
||||
self.config.flush()
|
||||
arguments = mock_out.call_args_list[-1][0][0]
|
||||
self.assertEquals('-e', arguments[0])
|
||||
self.assertTrue('default_parameter=new_value' in arguments)
|
||||
self.assertTrue('extra_param=another_value' in arguments)
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_flush_updates_object(self, mock_out):
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
self.config.flush()
|
||||
mock_out.reset_mock()
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
mock_out.assert_not_called()
|
||||
|
||||
@mock.patch('certbot_postfix.util.PostfixUtilBase._get_output')
|
||||
def test_flush_throws_error_on_fail(self, mock_out):
|
||||
mock_out.side_effect = [IOError("oh no!")]
|
||||
self.config.set('default_parameter', 'new_value')
|
||||
self.assertRaises(errors.PluginError, self.config.flush)
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
unittest.main()
|
||||
205
certbot-postfix/certbot_postfix/tests/util_test.py
Normal file
205
certbot-postfix/certbot_postfix/tests/util_test.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Tests for certbot_postfix.util."""
|
||||
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot import errors
|
||||
|
||||
|
||||
class PostfixUtilBaseTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.PostfixUtilBase."""
|
||||
|
||||
@classmethod
|
||||
def _create_object(cls, *args, **kwargs):
|
||||
from certbot_postfix.util import PostfixUtilBase
|
||||
return PostfixUtilBase(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot_postfix.util.verify_exe_exists')
|
||||
def test_no_exe(self, mock_verify):
|
||||
expected_error = errors.NoInstallationError
|
||||
mock_verify.side_effect = expected_error
|
||||
self.assertRaises(expected_error, self._create_object, 'nonexistent')
|
||||
|
||||
def test_object_creation(self):
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
self._create_object('existent')
|
||||
|
||||
@mock.patch('certbot_postfix.util.check_all_output')
|
||||
def test_call_extends_args(self, mock_output):
|
||||
# pylint: disable=protected-access
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
mock_output.return_value = 'expected'
|
||||
postfix = self._create_object('executable')
|
||||
postfix._call(['many', 'extra', 'args'])
|
||||
mock_output.assert_called_with(['executable', 'many', 'extra', 'args'])
|
||||
postfix._call()
|
||||
mock_output.assert_called_with(['executable'])
|
||||
|
||||
def test_create_with_config(self):
|
||||
# pylint: disable=protected-access
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
postfix = self._create_object('exec', 'config_dir')
|
||||
self.assertEqual(postfix._base_command, ['exec', '-c', 'config_dir'])
|
||||
|
||||
class PostfixUtilTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# pylint: disable=protected-access
|
||||
from certbot_postfix.util import PostfixUtil
|
||||
with mock.patch('certbot_postfix.util.verify_exe_exists'):
|
||||
self.postfix = PostfixUtil()
|
||||
self.postfix._call = mock.Mock()
|
||||
self.mock_call = self.postfix._call
|
||||
|
||||
def test_test(self):
|
||||
self.postfix.test()
|
||||
self.mock_call.assert_called_with(['check'])
|
||||
|
||||
def test_test_raises_error_when_check_fails(self):
|
||||
self.mock_call.side_effect = [subprocess.CalledProcessError(1, "")]
|
||||
self.assertRaises(errors.MisconfigurationError, self.postfix.test)
|
||||
self.mock_call.assert_called_with(['check'])
|
||||
|
||||
def test_restart_while_running(self):
|
||||
self.mock_call.side_effect = [subprocess.CalledProcessError(1, ""), None]
|
||||
self.postfix.restart()
|
||||
self.mock_call.assert_called_with(['start'])
|
||||
|
||||
def test_restart_while_not_running(self):
|
||||
self.postfix.restart()
|
||||
self.mock_call.assert_called_with(['reload'])
|
||||
|
||||
def test_restart_raises_error_when_reload_fails(self):
|
||||
self.mock_call.side_effect = [None, subprocess.CalledProcessError(1, "")]
|
||||
self.assertRaises(errors.PluginError, self.postfix.restart)
|
||||
self.mock_call.assert_called_with(['reload'])
|
||||
|
||||
def test_restart_raises_error_when_start_fails(self):
|
||||
self.mock_call.side_effect = [
|
||||
subprocess.CalledProcessError(1, ""),
|
||||
subprocess.CalledProcessError(1, "")]
|
||||
self.assertRaises(errors.PluginError, self.postfix.restart)
|
||||
self.mock_call.assert_called_with(['start'])
|
||||
|
||||
class CheckAllOutputTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.check_all_output."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from certbot_postfix.util import check_all_output
|
||||
return check_all_output(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot_postfix.util.logger')
|
||||
@mock.patch('certbot_postfix.util.subprocess.Popen')
|
||||
def test_command_error(self, mock_popen, mock_logger):
|
||||
command = 'foo'
|
||||
retcode = 42
|
||||
output = 'bar'
|
||||
err = 'baz'
|
||||
|
||||
mock_popen().communicate.return_value = (output, err)
|
||||
mock_popen().poll.return_value = 42
|
||||
|
||||
self.assertRaises(subprocess.CalledProcessError, self._call, command)
|
||||
log_args = mock_logger.debug.call_args[0]
|
||||
for value in (command, retcode, output, err,):
|
||||
self.assertTrue(value in log_args)
|
||||
|
||||
@mock.patch('certbot_postfix.util.subprocess.Popen')
|
||||
def test_success(self, mock_popen):
|
||||
command = 'foo'
|
||||
expected = ('bar', '')
|
||||
mock_popen().communicate.return_value = expected
|
||||
mock_popen().poll.return_value = 0
|
||||
|
||||
self.assertEqual(self._call(command), expected)
|
||||
|
||||
def test_stdout_error(self):
|
||||
self.assertRaises(ValueError, self._call, stdout=None)
|
||||
|
||||
def test_stderr_error(self):
|
||||
self.assertRaises(ValueError, self._call, stderr=None)
|
||||
|
||||
def test_universal_newlines_error(self):
|
||||
self.assertRaises(ValueError, self._call, universal_newlines=False)
|
||||
|
||||
|
||||
class VerifyExeExistsTest(unittest.TestCase):
|
||||
"""Tests for certbot_postfix.util.verify_exe_exists."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, *args, **kwargs):
|
||||
from certbot_postfix.util import verify_exe_exists
|
||||
return verify_exe_exists(*args, **kwargs)
|
||||
|
||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
||||
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
|
||||
def test_failure(self, mock_exe_exists, mock_path_surgery):
|
||||
mock_exe_exists.return_value = mock_path_surgery.return_value = False
|
||||
self.assertRaises(errors.NoInstallationError, self._call, 'foo')
|
||||
|
||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
||||
def test_simple_success(self, mock_exe_exists):
|
||||
mock_exe_exists.return_value = True
|
||||
self._call('foo')
|
||||
|
||||
@mock.patch('certbot_postfix.util.certbot_util.exe_exists')
|
||||
@mock.patch('certbot_postfix.util.plugins_util.path_surgery')
|
||||
def test_successful_surgery(self, mock_exe_exists, mock_path_surgery):
|
||||
mock_exe_exists.return_value = False
|
||||
mock_path_surgery.return_value = True
|
||||
self._call('foo')
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
""" Testing random utility functions in util.py
|
||||
"""
|
||||
def test_report_master_overrides(self):
|
||||
from certbot_postfix.util import report_master_overrides
|
||||
self.assertRaises(errors.PluginError, report_master_overrides, 'name',
|
||||
[('service/type', 'value')])
|
||||
# Shouldn't raise error
|
||||
report_master_overrides('name', [('service/type', 'value')],
|
||||
acceptable_overrides=('value',))
|
||||
|
||||
def test_no_acceptable_value(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
self.assertFalse(is_acceptable_value('name', 'value', None))
|
||||
|
||||
def test_is_acceptable_value(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
self.assertTrue(is_acceptable_value('name', 'value', ('value',)))
|
||||
self.assertFalse(is_acceptable_value('name', 'bad', ('value',)))
|
||||
|
||||
def test_is_acceptable_tuples(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
self.assertTrue(is_acceptable_value('name', 'value', ('value', 'value1')))
|
||||
self.assertFalse(is_acceptable_value('name', 'bad', ('value', 'value1')))
|
||||
|
||||
def test_is_acceptable_protocols(self):
|
||||
from certbot_postfix.util import is_acceptable_value
|
||||
# SSLv2 and SSLv3 are both not supported, unambiguously
|
||||
self.assertFalse(is_acceptable_value('tls_mandatory_protocols_lol',
|
||||
'SSLv2, SSLv3', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'SSLv2, SSLv3', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv2, !TLSv1', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv2, SSLv3, !SSLv3, ', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv2, !SSLv3', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'!SSLv3, !TLSv1, !SSLv2', None))
|
||||
# TLSv1.2 is supported unambiguously
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1, TLSv1.1,', None))
|
||||
self.assertFalse(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1.2, !TLSv1.2,', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1.2, ', None))
|
||||
self.assertTrue(is_acceptable_value('tls_protocols_lol',
|
||||
'TLSv1, TLSv1.1, TLSv1.2', None))
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
unittest.main()
|
||||
292
certbot-postfix/certbot_postfix/util.py
Normal file
292
certbot-postfix/certbot_postfix/util.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""Utility functions for use in the Postfix installer."""
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from certbot import errors
|
||||
from certbot import util as certbot_util
|
||||
from certbot.plugins import util as plugins_util
|
||||
from certbot_postfix import constants
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
COMMAND = "postfix"
|
||||
|
||||
class PostfixUtilBase(object):
|
||||
"""A base class for wrapping Postfix command line utilities."""
|
||||
|
||||
def __init__(self, executable, config_dir=None):
|
||||
"""Sets up the Postfix utility class.
|
||||
|
||||
:param str executable: name or path of the Postfix utility
|
||||
:param str config_dir: path to an alternative Postfix config
|
||||
|
||||
:raises .NoInstallationError: when the executable isn't found
|
||||
|
||||
"""
|
||||
self.executable = executable
|
||||
verify_exe_exists(executable)
|
||||
self._set_base_command(config_dir)
|
||||
self.config_dir = None
|
||||
|
||||
def _set_base_command(self, config_dir):
|
||||
self._base_command = [self.executable]
|
||||
if config_dir is not None:
|
||||
self._base_command.extend(('-c', config_dir,))
|
||||
|
||||
def _call(self, extra_args=None):
|
||||
"""Runs the Postfix utility and returns the result.
|
||||
|
||||
:param list extra_args: additional arguments for the command
|
||||
|
||||
:returns: data written to stdout and stderr
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
:raises subprocess.CalledProcessError: if the command fails
|
||||
|
||||
"""
|
||||
args = list(self._base_command)
|
||||
if extra_args is not None:
|
||||
args.extend(extra_args)
|
||||
return check_all_output(args)
|
||||
|
||||
def _get_output(self, extra_args=None):
|
||||
"""Runs the Postfix utility and returns only stdout output.
|
||||
|
||||
This function relies on self._call for running the utility.
|
||||
|
||||
:param list extra_args: additional arguments for the command
|
||||
|
||||
:returns: data written to stdout
|
||||
:rtype: str
|
||||
|
||||
:raises subprocess.CalledProcessError: if the command fails
|
||||
|
||||
"""
|
||||
return self._call(extra_args)[0]
|
||||
|
||||
class PostfixUtil(PostfixUtilBase):
|
||||
"""Wrapper around Postfix CLI tool.
|
||||
"""
|
||||
|
||||
def __init__(self, config_dir=None):
|
||||
super(PostfixUtil, self).__init__(COMMAND, config_dir)
|
||||
|
||||
def test(self):
|
||||
"""Make sure the configuration is valid.
|
||||
|
||||
:raises .MisconfigurationError: if the config is invalid
|
||||
"""
|
||||
try:
|
||||
self._call(["check"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.debug("Could not check postfix configuration:\n%s", e)
|
||||
raise errors.MisconfigurationError(
|
||||
"Postfix failed internal configuration check.")
|
||||
|
||||
def restart(self):
|
||||
"""Restart or refresh the server content.
|
||||
|
||||
:raises .PluginError: when server cannot be restarted
|
||||
|
||||
"""
|
||||
logger.info("Reloading Postfix configuration...")
|
||||
if self._is_running():
|
||||
self._reload()
|
||||
else:
|
||||
self._start()
|
||||
|
||||
|
||||
def _is_running(self):
|
||||
"""Is Postfix currently running?
|
||||
|
||||
Uses the 'postfix status' command to determine if Postfix is
|
||||
currently running using the specified configuration files.
|
||||
|
||||
:returns: True if Postfix is running, otherwise, False
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
try:
|
||||
self._call(["status"])
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _start(self):
|
||||
"""Instructions Postfix to start running.
|
||||
|
||||
:raises .PluginError: when Postfix cannot start
|
||||
|
||||
"""
|
||||
try:
|
||||
self._call(["start"])
|
||||
except subprocess.CalledProcessError:
|
||||
raise errors.PluginError("Postfix failed to start")
|
||||
|
||||
def _reload(self):
|
||||
"""Instructs Postfix to reload its configuration.
|
||||
|
||||
If Postfix isn't currently running, this method will fail.
|
||||
|
||||
:raises .PluginError: when Postfix cannot reload
|
||||
"""
|
||||
try:
|
||||
self._call(["reload"])
|
||||
except subprocess.CalledProcessError:
|
||||
raise errors.PluginError(
|
||||
"Postfix failed to reload its configuration")
|
||||
|
||||
def check_all_output(*args, **kwargs):
|
||||
"""A version of subprocess.check_output that also captures stderr.
|
||||
|
||||
This is the same as :func:`subprocess.check_output` except output
|
||||
written to stderr is also captured and returned to the caller. The
|
||||
return value is a tuple of two strings (rather than byte strings).
|
||||
To accomplish this, the caller cannot set the stdout, stderr, or
|
||||
universal_newlines parameters to :class:`subprocess.Popen`.
|
||||
|
||||
Additionally, if the command exits with a nonzero status, output is
|
||||
not included in the raised :class:`subprocess.CalledProcessError`
|
||||
because Python 2.6 does not support this. Instead, the failure
|
||||
including the output is logged.
|
||||
|
||||
:param tuple args: positional arguments for Popen
|
||||
:param dict kwargs: keyword arguments for Popen
|
||||
|
||||
:returns: data written to stdout and stderr
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
:raises ValueError: if arguments are invalid
|
||||
:raises subprocess.CalledProcessError: if the command fails
|
||||
|
||||
"""
|
||||
for keyword in ('stdout', 'stderr', 'universal_newlines',):
|
||||
if keyword in kwargs:
|
||||
raise ValueError(
|
||||
keyword + ' argument not allowed, it will be overridden.')
|
||||
|
||||
kwargs['stdout'] = subprocess.PIPE
|
||||
kwargs['stderr'] = subprocess.PIPE
|
||||
kwargs['universal_newlines'] = True
|
||||
|
||||
process = subprocess.Popen(*args, **kwargs)
|
||||
output, err = process.communicate()
|
||||
retcode = process.poll()
|
||||
if retcode:
|
||||
cmd = kwargs.get('args')
|
||||
if cmd is None:
|
||||
cmd = args[0]
|
||||
logger.debug(
|
||||
"'%s' exited with %d. stdout output was:\n%s\nstderr output was:\n%s",
|
||||
cmd, retcode, output, err)
|
||||
raise subprocess.CalledProcessError(retcode, cmd)
|
||||
return (output, err)
|
||||
|
||||
|
||||
def verify_exe_exists(exe, message=None):
|
||||
"""Ensures an executable with the given name is available.
|
||||
|
||||
If an executable isn't found for the given path or name, extra
|
||||
directories are added to the user's PATH to help find system
|
||||
utilities that may not be available in the default cron PATH.
|
||||
|
||||
:param str exe: executable path or name
|
||||
:param str message: Error message to print.
|
||||
|
||||
:raises .NoInstallationError: when the executable isn't found
|
||||
|
||||
"""
|
||||
if message is None:
|
||||
message = "Cannot find executable '{0}'.".format(exe)
|
||||
if not (certbot_util.exe_exists(exe) or plugins_util.path_surgery(exe)):
|
||||
raise errors.NoInstallationError(message)
|
||||
|
||||
def report_master_overrides(name, overrides, acceptable_overrides=None):
|
||||
"""If the value for a parameter `name` is overridden by other services,
|
||||
report a warning to notify the user. If `parameter` is a TLS version parameter
|
||||
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
|
||||
`acceptable_overrides` isn't used each value in overrides is inspected for secure TLS
|
||||
versions.
|
||||
|
||||
:param str name: The name of the parameter that is being overridden.
|
||||
:param list overrides: The values that other services are setting for `name`.
|
||||
Each override is a tuple: (service name, value)
|
||||
:param tuple acceptable_overrides: Override values that are acceptable. For instance, if
|
||||
another service is overriding our parameter with a more secure option, we don't have
|
||||
to warn. If this is set to None, errors are raised for *any* overrides of `name`!
|
||||
"""
|
||||
error_string = ""
|
||||
for override in overrides:
|
||||
service, value = override
|
||||
# If this override is acceptable:
|
||||
if acceptable_overrides is not None and \
|
||||
is_acceptable_value(name, value, acceptable_overrides):
|
||||
continue
|
||||
error_string += " {0}: {1}\n".format(service, value)
|
||||
if error_string:
|
||||
raise errors.PluginError("{0} is overridden with less secure options by the "
|
||||
"following services in master.cf:\n".format(name) + error_string)
|
||||
|
||||
|
||||
def is_acceptable_value(parameter, value, acceptable=None):
|
||||
""" Returns whether the `value` for this `parameter` is acceptable,
|
||||
given a tuple of `acceptable` values. If `parameter` is a TLS version parameter
|
||||
(i.e., `parameter` contains 'tls_protocols' or 'tls_mandatory_protocols'), then
|
||||
`acceptable` isn't used and `value` is inspected for secure TLS versions.
|
||||
|
||||
:param str parameter: The name of the parameter being set.
|
||||
:param str value: Proposed new value for parameter.
|
||||
:param tuple acceptable: List of acceptable values for parameter.
|
||||
"""
|
||||
# Check if param value is a comma-separated list of protocols.
|
||||
# Otherwise, just check whether the value is in the acceptable list.
|
||||
if 'tls_protocols' in parameter or 'tls_mandatory_protocols' in parameter:
|
||||
return _has_acceptable_tls_versions(value)
|
||||
if acceptable is not None:
|
||||
return value in acceptable
|
||||
return False
|
||||
|
||||
|
||||
def _has_acceptable_tls_versions(parameter_string):
|
||||
"""
|
||||
Checks to see if the list of TLS protocols is acceptable.
|
||||
This requires that TLSv1.2 is supported, and neither SSLv2 nor SSLv3 are supported.
|
||||
|
||||
Should be a string of protocol names delimited by commas, spaces, or colons.
|
||||
|
||||
Postfix's documents suggest listing protocols to exclude, like "!SSLv2, !SSLv3".
|
||||
Listing the protocols to include, like "TLSv1, TLSv1.1, TLSv1.2" is okay as well,
|
||||
though not recommended
|
||||
|
||||
When these two modes are interspersed, the presence of a single non-negated protocol name
|
||||
(i.e. "TLSv1" rather than "!TLSv1") automatically excludes all other unnamed protocols.
|
||||
|
||||
In addition, the presence of both a protocol name inclusion and exclusion isn't explicitly
|
||||
documented, so this method should return False if it encounters contradicting statements
|
||||
about TLSv1.2, SSLv2, or SSLv3. (for instance, "SSLv3, !SSLv3").
|
||||
"""
|
||||
if not parameter_string:
|
||||
return False
|
||||
bad_versions = list(constants.TLS_VERSIONS)
|
||||
for version in constants.ACCEPTABLE_TLS_VERSIONS:
|
||||
del bad_versions[bad_versions.index(version)]
|
||||
supported_version_list = re.split("[, :]+", parameter_string)
|
||||
# The presence of any non-"!" protocol listing excludes the others by default.
|
||||
inclusion_list = False
|
||||
for version in supported_version_list:
|
||||
if not version:
|
||||
continue
|
||||
if version in bad_versions: # short-circuit if we recognize any bad version
|
||||
return False
|
||||
if version[0] != "!":
|
||||
inclusion_list = True
|
||||
if inclusion_list: # For any inclusion list, we still require TLS 1.2.
|
||||
if "TLSv1.2" not in supported_version_list or "!TLSv1.2" in supported_version_list:
|
||||
return False
|
||||
else:
|
||||
for bad_version in bad_versions:
|
||||
if "!" + bad_version not in supported_version_list:
|
||||
return False
|
||||
return True
|
||||
|
||||
1
certbot-postfix/docs/.gitignore
vendored
Normal file
1
certbot-postfix/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_build/
|
||||
20
certbot-postfix/docs/Makefile
Normal file
20
certbot-postfix/docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = certbot-postfix
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
8
certbot-postfix/docs/api.rst
Normal file
8
certbot-postfix/docs/api.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=================
|
||||
API Documentation
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
api/**
|
||||
5
certbot-postfix/docs/api/installer.rst
Normal file
5
certbot-postfix/docs/api/installer.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_postfix.installer`
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: certbot_postfix.installer
|
||||
:members:
|
||||
5
certbot-postfix/docs/api/postconf.rst
Normal file
5
certbot-postfix/docs/api/postconf.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
:mod:`certbot_postfix.postconf`
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: certbot_postfix.postconf
|
||||
:members:
|
||||
190
certbot-postfix/docs/conf.py
Normal file
190
certbot-postfix/docs/conf.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'certbot-postfix'
|
||||
copyright = u'2018, Certbot Project'
|
||||
author = u'Certbot Project'
|
||||
|
||||
# The short X.Y version
|
||||
version = u'0'
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = u'0'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_default_flags = ['show-inheritance', 'private-members']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = u'en'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path .
|
||||
exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
default_role = 'py:obj'
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
|
||||
# http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs
|
||||
# on_rtd is whether we are on readthedocs.org
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
# otherwise, readthedocs.org uses their theme by default, so no need to specify it
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'certbot-postfixdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'certbot-postfix.tex', u'certbot-postfix Documentation',
|
||||
u'Certbot Project', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'certbot-postfix', u'certbot-postfix Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'certbot-postfix', u'certbot-postfix Documentation',
|
||||
author, 'certbot-postfix', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# -- Options for intersphinx extension ---------------------------------------
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'certbot': ('https://certbot.eff.org/docs/', None),
|
||||
}
|
||||
|
||||
# -- Options for todo extension ----------------------------------------------
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
28
certbot-postfix/docs/index.rst
Normal file
28
certbot-postfix/docs/index.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
.. certbot-postfix documentation master file, created by
|
||||
sphinx-quickstart on Wed May 2 16:01:06 2018.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to certbot-postfix's documentation!
|
||||
===========================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. automodule:: certbot_postfix
|
||||
:members:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
36
certbot-postfix/docs/make.bat
Normal file
36
certbot-postfix/docs/make.bat
Normal file
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=certbot-postfix
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
2
certbot-postfix/local-oldest-requirements.txt
Normal file
2
certbot-postfix/local-oldest-requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
acme[dev]==0.25.0
|
||||
certbot[dev]==0.23.0
|
||||
2
certbot-postfix/setup.cfg
Normal file
2
certbot-postfix/setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
63
certbot-postfix/setup.py
Normal file
63
certbot-postfix/setup.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.26.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'acme>=0.25.0',
|
||||
'certbot>0.23.0',
|
||||
'setuptools',
|
||||
'six',
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
docs_extras = [
|
||||
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='certbot-postfix',
|
||||
version=version,
|
||||
description="Postfix plugin for Certbot",
|
||||
url='https://github.com/certbot/certbot',
|
||||
author="Certbot Project",
|
||||
author_email='client-dev@letsencrypt.org',
|
||||
license='Apache License 2.0',
|
||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Plugins',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Communications :: Email :: Mail Transport Agents',
|
||||
'Topic :: Security',
|
||||
'Topic :: System :: Installation/Setup',
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: System :: Systems Administration',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'certbot.plugins': [
|
||||
'postfix = certbot_postfix:Installer',
|
||||
],
|
||||
},
|
||||
test_suite='certbot_postfix',
|
||||
)
|
||||
@@ -36,6 +36,7 @@ class PluginEntryPoint(object):
|
||||
"certbot-dns-rfc2136",
|
||||
"certbot-dns-route53",
|
||||
"certbot-nginx",
|
||||
"certbot-postfix",
|
||||
]
|
||||
"""Distributions for which prefix will be omitted."""
|
||||
|
||||
|
||||
@@ -25,5 +25,6 @@ fi
|
||||
-e certbot-dns-rfc2136 \
|
||||
-e certbot-dns-route53 \
|
||||
-e certbot-nginx \
|
||||
-e certbot-postfix \
|
||||
-e letshelp-certbot \
|
||||
-e certbot-compatibility-test
|
||||
|
||||
@@ -23,5 +23,6 @@ fi
|
||||
-e certbot-dns-nsone \
|
||||
-e certbot-dns-route53 \
|
||||
-e certbot-nginx \
|
||||
-e certbot-postfix \
|
||||
-e letshelp-certbot \
|
||||
-e certbot-compatibility-test
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# -e makes sure we fail fast and don't submit coveralls submit
|
||||
|
||||
if [ "xxx$1" = "xxx" ]; then
|
||||
pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx letshelp_certbot"
|
||||
pkgs="certbot acme certbot_apache certbot_dns_cloudflare certbot_dns_cloudxns certbot_dns_digitalocean certbot_dns_dnsimple certbot_dns_dnsmadeeasy certbot_dns_google certbot_dns_luadns certbot_dns_nsone certbot_dns_rfc2136 certbot_dns_route53 certbot_nginx certbot_postfix letshelp_certbot"
|
||||
else
|
||||
pkgs="$@"
|
||||
fi
|
||||
@@ -43,6 +43,8 @@ cover () {
|
||||
min=92
|
||||
elif [ "$1" = "certbot_nginx" ]; then
|
||||
min=97
|
||||
elif [ "$1" = "certbot_postfix" ]; then
|
||||
min=100
|
||||
elif [ "$1" = "letshelp_certbot" ]; then
|
||||
min=100
|
||||
else
|
||||
|
||||
2
tox.ini
2
tox.ini
@@ -31,6 +31,7 @@ all_packages =
|
||||
certbot-apache \
|
||||
{[base]dns_packages} \
|
||||
certbot-nginx \
|
||||
certbot-postfix \
|
||||
letshelp-certbot
|
||||
install_packages =
|
||||
{toxinidir}/tools/pip_install_editable.sh {[base]all_packages}
|
||||
@@ -50,6 +51,7 @@ source_paths =
|
||||
certbot-dns-rfc2136/certbot_dns_rfc2136
|
||||
certbot-dns-route53/certbot_dns_route53
|
||||
certbot-nginx/certbot_nginx
|
||||
certbot-postfix/certbot_postfix
|
||||
letshelp-certbot/letshelp_certbot
|
||||
tests/lock_test.py
|
||||
|
||||
|
||||
Reference in New Issue
Block a user