Compare commits
642 Commits
Author | SHA1 | Date |
---|---|---|
lub | 2506bc3a7f | 1 year ago |
bors[bot] | b243ea084d | 1 year ago |
Florent Daigniere | 5790b0a84b | 1 year ago |
Dimitri Huisman | 6d31831cf5 | 1 year ago |
Dimitri Huisman | 709edb522b | 1 year ago |
bors[bot] | 6f3a01e31c | 1 year ago |
Florent Daigniere | 46e803fdff | 1 year ago |
Florent Daigniere | 61a40e203a | 1 year ago |
Florent Daigniere | cb5e0934cb | 1 year ago |
Florent Daigniere | c9df6161ba | 1 year ago |
Florent Daigniere | 8391936dc2 | 1 year ago |
Dimitri Huisman | a2c811d28a | 1 year ago |
Dimitri Huisman | 1d2053204a | 1 year ago |
Dimitri Huisman | 64a132fdd9 | 1 year ago |
Dimitri Huisman | 5bd528319b | 1 year ago |
bors[bot] | 5044c78740 | 1 year ago |
bors[bot] | c15595836a | 1 year ago |
Dimitri Huisman | ee1f0f94a3 | 1 year ago |
Dimitri Huisman | f20208fb4b | 1 year ago |
Dimitri Huisman | 4912fa1dff | 1 year ago |
Dimitri Huisman | 20bf0e8a65 | 1 year ago |
Dimitri Huisman | 29bfc9dd9d | 1 year ago |
Dimitri Huisman | 25b9db4b00 | 1 year ago |
bors[bot] | 1d9791ceaa | 1 year ago |
bors[bot] | 00533d9eea | 1 year ago |
Dimitri Huisman | 0d048d24d3 | 1 year ago |
bors[bot] | 04a1868a5e | 1 year ago |
bors[bot] | 5fbfb3cb1c | 1 year ago |
Dimitri Huisman | c6c2805196 | 1 year ago |
Dimitri Huisman | da4934847f | 1 year ago |
Florent Daigniere | 459694f4a2 | 1 year ago |
bors[bot] | 1d360055b7 | 1 year ago |
bors[bot] | 86ad4c93a9 | 1 year ago |
Dimitri Huisman | c482c71f6c | 1 year ago |
Dimitri Huisman | 06ac7f507d | 1 year ago |
Dimitri Huisman | 120cd34989 | 1 year ago |
Florent Daigniere | 698f1f377c | 1 year ago |
Florent Daigniere | 8eb1542f64 | 1 year ago |
Dimitri Huisman | 31faee4218 | 1 year ago |
bors[bot] | 03ff2f2132 | 1 year ago |
Dimitri Huisman | b99828c4f6 | 1 year ago |
Florent Daigniere | 22bb0594da | 1 year ago |
bors[bot] | b30540c074 | 1 year ago |
Dario Ernst | 384d11ddaa | 1 year ago |
Florent Daigniere | 1831ca3b1e | 1 year ago |
Florent Daigniere | e1739befc0 | 1 year ago |
bors[bot] | 31a85397dd | 1 year ago |
Florent Daigniere | f44cd24bf8 | 1 year ago |
Florent Daigniere | 925c753f40 | 1 year ago |
Florent Daigniere | b607375603 | 1 year ago |
Florent Daigniere | dd912169fb | 1 year ago |
Florent Daigniere | 1b045b4a94 | 1 year ago |
bors[bot] | f0b3689732 | 1 year ago |
Dimitri Huisman | 17c68ca86e | 1 year ago |
bors[bot] | f660ab568e | 1 year ago |
Florent Daigniere | d9527e561e | 1 year ago |
Florent Daigniere | 331bda3822 | 1 year ago |
Florent Daigniere | 61ca539d6d | 1 year ago |
bors[bot] | 0069b67ada | 1 year ago |
Florent Daigniere | cef97f78f1 | 1 year ago |
Florent Daigniere | a973fffa9e | 1 year ago |
Florent Daigniere | 7d21966114 | 1 year ago |
Dimitri Huisman | 45177bd25a | 1 year ago |
Dimitri Huisman | 7ce28bd6e9 | 1 year ago |
Dimitri Huisman | 8861ce6edb | 1 year ago |
S474N | 92be819053 | 1 year ago |
S474N | d6757514af | 1 year ago |
bors[bot] | 0de2430868 | 1 year ago |
bors[bot] | 8c873d3700 | 1 year ago |
Florent Daigniere | b205f406de | 1 year ago |
Florent Daigniere | 2cf4e61fd2 | 1 year ago |
Florent Daigniere | 8b502b73ee | 1 year ago |
Florent Daigniere | 511cdcf1ba | 1 year ago |
Florent Daigniere | 8d8f753796 | 1 year ago |
Florent Daigniere | dd21d4bf0c | 1 year ago |
Florent Daigniere | 07da831533 | 1 year ago |
Florent Daigniere | 23ae60e3df | 1 year ago |
bors[bot] | b50c858823 | 1 year ago |
Florent Daigniere | fed5ab1564 | 1 year ago |
score | bcf2dd8794 | 1 year ago |
Florent Daigniere | b983c64b4b | 1 year ago |
Florent Daigniere | bb5d007882 | 1 year ago |
Florent Daigniere | 6a4d8603fc | 1 year ago |
Florent Daigniere | f125420400 | 1 year ago |
Dimitri Huisman | 9dffa11f0f | 1 year ago |
Florent Daigniere | 65288d7291 | 1 year ago |
Florent Daigniere | b623e1f286 | 1 year ago |
Florent Daigniere | c55a06f85d | 1 year ago |
Florent Daigniere | 6191d3b59e | 1 year ago |
Florent Daigniere | 0141a7500f | 1 year ago |
bors[bot] | ae3f656923 | 1 year ago |
Florent Daigniere | 66b7c76836 | 1 year ago |
bors[bot] | aea7407044 | 1 year ago |
Florent Daigniere | bb127d15ff | 1 year ago |
Florent Daigniere | d20c217ae6 | 1 year ago |
Florent Daigniere | 83cc23a51a | 1 year ago |
bors[bot] | 46429ab247 | 1 year ago |
Florent Daigniere | 9ef96e9c1e | 1 year ago |
Nico Winkelsträter | 9cb2ef7632 | 1 year ago |
Florent Daigniere | 085bac6e08 | 1 year ago |
bors[bot] | 712f14a07b | 1 year ago |
Alexander Graf | fa084d7b1c | 1 year ago |
Alexander Graf | d017b3f22a | 1 year ago |
bors[bot] | 47fcf7de2d | 1 year ago |
Florent Daigniere | caa8412d82 | 1 year ago |
bors[bot] | 3804d0bf5e | 1 year ago |
Florent Daigniere | 0ec9f1797f | 1 year ago |
Florent Daigniere | 294ac4adb2 | 1 year ago |
Florent Daigniere | 35e9bfb8ab | 1 year ago |
Florent Daigniere | d30f71234d | 1 year ago |
Florent Daigniere | a60159a0db | 1 year ago |
Florent Daigniere | e2a25c79fc | 1 year ago |
Alexander Graf | fdb819852e | 1 year ago |
bors[bot] | 5b4f2fb075 | 1 year ago |
Dimitri Huisman | 44ad14811d | 1 year ago |
Dimitri Huisman | d9a6777d9d | 1 year ago |
bors[bot] | 4a24bd9e24 | 1 year ago |
Dimitri Huisman | 7bcac3bbaa | 1 year ago |
bors[bot] | 71d4c63c86 | 1 year ago |
Alexander Graf | ab5caac6f7 | 1 year ago |
Dimitri Huisman | 75afe1092d | 1 year ago |
bors[bot] | 600e0c2203 | 1 year ago |
Dimitri Huisman | 2ccdfb9a6b | 1 year ago |
Dimitri Huisman | 0673d32306 | 1 year ago |
bors[bot] | 8cd5c462f8 | 1 year ago |
Alexander Graf | 50fc1cb8b3 | 1 year ago |
Alexander Graf | 8f425ce081 | 1 year ago |
Alexander Graf | f00059d10c | 1 year ago |
Alexander Graf | 8b0b87984d | 1 year ago |
Alexander Graf | 2fa0461803 | 1 year ago |
Alexander Graf | 31e974f829 | 1 year ago |
Alexander Graf | 3af3aa9395 | 1 year ago |
Alexander Graf | 65595d139a | 1 year ago |
Alexander Graf | 3c9c01f8eb | 1 year ago |
bors[bot] | 3a1cecbe21 | 1 year ago |
Florent Daigniere | ae7061c561 | 1 year ago |
bors[bot] | 8cffee55be | 1 year ago |
Florent Daigniere | 802ab533d2 | 1 year ago |
Florent Daigniere | 61f6e6018b | 1 year ago |
Florent Daigniere | e326393f03 | 1 year ago |
bors[bot] | 9bd76536a1 | 1 year ago |
Alexander Graf | 21ac230cce | 1 year ago |
Alexander Graf | 84d156d02f | 1 year ago |
Alexander Graf | 25635396e7 | 1 year ago |
Alexander Graf | 120a7e8368 | 1 year ago |
Alexander Graf | 842be9b7c3 | 1 year ago |
Alexander Graf | 1ad1d8d95d | 1 year ago |
Chris | 7cc5d1f756 | 1 year ago |
Alexander Graf | 8b1eb020e2 | 1 year ago |
Chris Schäpers | 35331a4295 | 1 year ago |
Chris | 9f6848110a | 1 year ago |
bors[bot] | e1a85a450f | 1 year ago |
Florent Daigniere | 926570f1ca | 1 year ago |
Florent Daigniere | 9803c51d55 | 1 year ago |
Florent Daigniere | 6533f41f48 | 1 year ago |
Florent Daigniere | 760ec301e3 | 1 year ago |
Florent Daigniere | 9d2046f43f | 1 year ago |
bors[bot] | db2a490256 | 1 year ago |
bors[bot] | 3ffe1d2a9e | 1 year ago |
Florent Daigniere | 46f05cb651 | 1 year ago |
Florent Daigniere | 5304311e0e | 1 year ago |
Florent Daigniere | 36623188b5 | 1 year ago |
bors[bot] | 179c624116 | 1 year ago |
Dimitri Huisman | 8cb7265eb2 | 1 year ago |
Alexander Graf | dd80fde841 | 1 year ago |
Alexander Graf | 30efdf557f | 1 year ago |
bors[bot] | 43e500faf5 | 1 year ago |
Alexander Graf | 02c4862427 | 1 year ago |
Dimitri Huisman | 18b900699c | 1 year ago |
Dimitri Huisman | d6e7314f05 | 1 year ago |
bors[bot] | cc6c808838 | 1 year ago |
Alexander Graf | c4ca1cffaf | 1 year ago |
Florent Daigniere | e43f6524ea | 1 year ago |
Alexander Graf | 5c968256e6 | 1 year ago |
bors[bot] | 151601744f | 1 year ago |
bors[bot] | 6d994525c4 | 1 year ago |
Alexander Graf | 10562233ca | 1 year ago |
bors[bot] | 7e60ba4e98 | 1 year ago |
Alexander Graf | 1697da6e23 | 1 year ago |
bors[bot] | dae9e9242b | 1 year ago |
bors[bot] | bbf0ac5d47 | 1 year ago |
Alexander Graf | 712679b4d8 | 1 year ago |
bors[bot] | cfebfdbd1f | 1 year ago |
Alexander Graf | d558be20f6 | 1 year ago |
Alexander Graf | 3b08b113bf | 1 year ago |
Dimitri Huisman | b0569035ae | 1 year ago |
bors[bot] | 3acec43224 | 1 year ago |
Vetési Zoltán | e76e857ae7 | 1 year ago |
bors[bot] | e857d242d6 | 1 year ago |
Florent Daigniere | 052f8e41ba | 1 year ago |
bors[bot] | 4295eafb64 | 1 year ago |
Florent Daigniere | ee6975b109 | 1 year ago |
Dimitri Huisman | bcceac359d | 1 year ago |
Florent Daigniere | 9d555b0eec | 1 year ago |
bors[bot] | da36bc585f | 1 year ago |
Florent Daigniere | 8b9bb350ec | 1 year ago |
Florent Daigniere | f18776fa0f | 1 year ago |
Florent Daigniere | e85a2a7e99 | 1 year ago |
Florent Daigniere | 92c0016e32 | 1 year ago |
Florent Daigniere | b263db72df | 1 year ago |
Florent Daigniere | bf0c345bb9 | 1 year ago |
bors[bot] | 44d36cbb8b | 1 year ago |
Łukasz Sitarski | 202ff8db14 | 1 year ago |
bors[bot] | e166550bd7 | 1 year ago |
Florent Daigniere | 4d80c95c41 | 1 year ago |
bors[bot] | bba6c5bb88 | 1 year ago |
bors[bot] | 52c17411bd | 1 year ago |
Florent Daigniere | ca44ccbe1c | 1 year ago |
bors[bot] | fe2b0bedb7 | 1 year ago |
Alexander Graf | 6f71ea833b | 1 year ago |
Florent Daigniere | edd303f54d | 1 year ago |
bors[bot] | 874e58348f | 1 year ago |
Florent Daigniere | cd107182c1 | 1 year ago |
Florent Daigniere | 8539344331 | 1 year ago |
Florent Daigniere | 36b3a9f4fb | 1 year ago |
Florent Daigniere | 83ea708490 | 1 year ago |
Florent Daigniere | 7a2d06401a | 1 year ago |
Florent Daigniere | 163261d951 | 1 year ago |
Florent Daigniere | 55c1e55529 | 1 year ago |
Florent Daigniere | 4ae0d7d768 | 1 year ago |
bors[bot] | c729954b4a | 1 year ago |
Alexander Graf | be40781394 | 1 year ago |
Alexander Graf | 2f0f46c8fa | 1 year ago |
Alexander Graf | 84ebab2cb4 | 1 year ago |
Dimitri Huisman | 3cb8358090 | 1 year ago |
Dimitri Huisman | 39b0d44079 | 1 year ago |
Dimitri Huisman | f9b26bd934 | 1 year ago |
Dimitri Huisman | 6347c18f8a | 1 year ago |
Dimitri Huisman | 61d092922c | 1 year ago |
Dimitri Huisman | afb224e796 | 1 year ago |
Dimitri Huisman | d4e5db5084 | 1 year ago |
Dimitri Huisman | 46d07ec236 | 1 year ago |
Dimitri Huisman | 67c423d61f | 1 year ago |
Dimitri Huisman | 7a36f6bbb9 | 1 year ago |
Dimitri Huisman | 5c9cdfe1de | 1 year ago |
Alexander Graf | 866ad89dfc | 1 year ago |
Alexander Graf | c30944404d | 1 year ago |
bors[bot] | e9175da586 | 1 year ago |
Florent Daigniere | 108958cabb | 1 year ago |
bors[bot] | 8d2bd6d9ff | 1 year ago |
Dimitri Huisman | 6d87fa423c | 1 year ago |
Dimitri Huisman | 33497c8e31 | 1 year ago |
bors[bot] | 8461a11ff4 | 1 year ago |
bors[bot] | caa27ede4b | 1 year ago |
Johnson Thiang | bd20ef04cc | 1 year ago |
Shamil Nunhuck | 5264a3070b | 1 year ago |
Shamil Nunhuck | 7225cb0d3e | 1 year ago |
bors[bot] | 23b09518db | 1 year ago |
Alexander Graf | 15ba442477 | 1 year ago |
Alexander Graf | 5a99ab316d | 1 year ago |
Alexander Graf | 373488148b | 1 year ago |
Alexander Graf | 36a567c783 | 1 year ago |
Alexander Graf | c38e6aae4e | 1 year ago |
Florent Daigniere | 6370d03f80 | 1 year ago |
Florent Daigniere | ef123f1b53 | 1 year ago |
Florent Daigniere | 49d458a0f3 | 1 year ago |
Florent Daigniere | 26858b110a | 1 year ago |
Florent Daigniere | 6241fbeb78 | 1 year ago |
Florent Daigniere | cea533ae57 | 1 year ago |
Florent Daigniere | f04be00798 | 1 year ago |
Florent Daigniere | 43bf068be2 | 1 year ago |
bors[bot] | 4315227215 | 1 year ago |
Florent Daigniere | 44c064ff38 | 1 year ago |
Florent Daigniere | b70be29403 | 1 year ago |
Florent Daigniere | 77d770a2d2 | 1 year ago |
bors[bot] | 251db0b1af | 1 year ago |
Florent Daigniere | df924b0864 | 1 year ago |
Florent Daigniere | 0fa239da11 | 1 year ago |
Florent Daigniere | c634b9ac04 | 1 year ago |
Florent Daigniere | 170b12baf0 | 1 year ago |
bors[bot] | 79f01c4e33 | 1 year ago |
bors[bot] | 59220ac83b | 1 year ago |
fastlorenzo | 135207db3e | 1 year ago |
fastlorenzo | 2fa8dcb51d | 1 year ago |
bors[bot] | 50c7fa882e | 1 year ago |
bors[bot] | f169f81436 | 1 year ago |
Florent Daigniere | e42d029c25 | 1 year ago |
Florent Daigniere | ae6af92b1d | 1 year ago |
Florent Daigniere | b630355d03 | 1 year ago |
Florent Daigniere | 4e3874b0c1 | 1 year ago |
bors[bot] | 1a67921b7c | 1 year ago |
Florent Daigniere | dfaba5bb17 | 1 year ago |
fastlorenzo | 0209825277 | 1 year ago |
Florent Daigniere | 8150ca77b2 | 1 year ago |
Florent Daigniere | 622e093122 | 1 year ago |
Florent Daigniere | 73107ba112 | 1 year ago |
Florent Daigniere | 619a5fbda2 | 1 year ago |
bors[bot] | 0bfe3f92a6 | 1 year ago |
bors[bot] | 8c3da2815d | 1 year ago |
bors[bot] | cd5e6c896f | 1 year ago |
bors[bot] | e20efc5b99 | 1 year ago |
Florent Daigniere | c565e69a01 | 1 year ago |
Florent Daigniere | b553d025eb | 1 year ago |
Florent Daigniere | 00f07ef533 | 1 year ago |
Florent Daigniere | 3e38e7b89d | 1 year ago |
Florent Daigniere | 83ef6d773d | 1 year ago |
Florent Daigniere | 98f16b1d47 | 1 year ago |
bors[bot] | a366116cae | 1 year ago |
Florent Daigniere | 5da2ab8fd1 | 1 year ago |
Florent Daigniere | bf588d19a4 | 1 year ago |
Florent Daigniere | 86edc3a919 | 1 year ago |
bors[bot] | b49d9ce243 | 1 year ago |
Florent Daigniere | c1062f3db2 | 1 year ago |
bors[bot] | 0bde746610 | 1 year ago |
bors[bot] | 033889dc95 | 1 year ago |
bors[bot] | e0d42cadc0 | 1 year ago |
Alexander Graf | b0990460a4 | 1 year ago |
Alexander Graf | 53720876b4 | 1 year ago |
Alexander Graf | a5eeab37e1 | 1 year ago |
Florent Daigniere | e927426dfa | 1 year ago |
Alexander Graf | 7828115102 | 1 year ago |
bors[bot] | 0e0ac201fc | 1 year ago |
Florent Daigniere | c4595fddca | 1 year ago |
Florent Daigniere | 9566c297d9 | 1 year ago |
Florent Daigniere | 8cba012546 | 1 year ago |
Florent Daigniere | b3f534a6ac | 1 year ago |
Florent Daigniere | d0631558c7 | 1 year ago |
Florent Daigniere | 3721a6aa02 | 1 year ago |
bors[bot] | 2104c04e3b | 1 year ago |
Florent Daigniere | 4c3c628ca4 | 1 year ago |
Florent Daigniere | 19bd9362d3 | 1 year ago |
Florent Daigniere | f1e5044dbe | 1 year ago |
bors[bot] | a8630c5a3b | 1 year ago |
Florent Daigniere | 02f2679dc4 | 1 year ago |
Florent Daigniere | b08d940d09 | 1 year ago |
Florent Daigniere | d77bf119f8 | 1 year ago |
Florent Daigniere | a8061f3ed3 | 1 year ago |
Florent Daigniere | 12117cef37 | 1 year ago |
Florent Daigniere | 612db96209 | 1 year ago |
Florent Daigniere | 709023ab5a | 1 year ago |
Florent Daigniere | 3bdc57adbc | 1 year ago |
Florent Daigniere | 32d44b96c3 | 1 year ago |
Florent Daigniere | e43effab63 | 1 year ago |
Florent Daigniere | d793c5eed8 | 1 year ago |
Florent Daigniere | 1327f34c2c | 1 year ago |
Florent Daigniere | e03d91a1ec | 1 year ago |
Florent Daigniere | 9fcff5e745 | 1 year ago |
Florent Daigniere | 63a12d9857 | 1 year ago |
Florent Daigniere | 546884d10c | 1 year ago |
bors[bot] | 5a7d73dc3d | 1 year ago |
Florent Daigniere | 7e1ab7978e | 1 year ago |
Florent Daigniere | 4881e0db2a | 1 year ago |
Florent Daigniere | c1144612be | 1 year ago |
Florent Daigniere | 4d8bd210c5 | 1 year ago |
Florent Daigniere | ee512112fb | 1 year ago |
Florent Daigniere | adacf579fc | 1 year ago |
Florent Daigniere | 3e45a791cf | 1 year ago |
bors[bot] | 9c6e9b05db | 1 year ago |
Florent Daigniere | 9fa3a3e0c7 | 1 year ago |
Florent Daigniere | e94f6eaf33 | 1 year ago |
Florent Daigniere | 9e61a33cb2 | 1 year ago |
bors[bot] | 6a3daa75ac | 1 year ago |
Florent Daigniere | f994c8687e | 1 year ago |
Florent Daigniere | 44c47586ea | 1 year ago |
Florent Daigniere | d3d7916b58 | 1 year ago |
bors[bot] | c1da586444 | 1 year ago |
Florent Daigniere | ab852772f9 | 1 year ago |
Florent Daigniere | 28d720bbc9 | 1 year ago |
bors[bot] | d650a9cc0f | 1 year ago |
Florent Daigniere | 45b01db9de | 1 year ago |
Florent Daigniere | 3fc0a0e7fa | 1 year ago |
Florent Daigniere | 4da2db1b0b | 1 year ago |
Florent Daigniere | c79e8d3852 | 1 year ago |
bors[bot] | 553b02fb3d | 1 year ago |
bors[bot] | 31c6c26ec8 | 1 year ago |
bors[bot] | 604eb69122 | 1 year ago |
Florent Daigniere | dcf11aea48 | 1 year ago |
Florent Daigniere | db9ed1fd59 | 1 year ago |
Florent Daigniere | f802601a08 | 1 year ago |
Florent Daigniere | d5ac9199a0 | 1 year ago |
Florent Daigniere | 7822b41048 | 1 year ago |
Florent Daigniere | ef9cc3c866 | 1 year ago |
Florent Daigniere | 38507b2e1b | 1 year ago |
Florent Daigniere | 6a22c82c02 | 1 year ago |
Florent Daigniere | cf7404e26c | 1 year ago |
Florent Daigniere | b20bf996ec | 1 year ago |
Florent Daigniere | e2d4e3eb2e | 1 year ago |
Florent Daigniere | 840b2bd9df | 1 year ago |
Florent Daigniere | 017ea5298e | 1 year ago |
Florent Daigniere | 2a4f6836cf | 1 year ago |
Florent Daigniere | e5ab9821f9 | 1 year ago |
Florent Daigniere | bdc085048d | 1 year ago |
Florent Daigniere | b28798c74f | 1 year ago |
Florent Daigniere | 1bfab1dbfa | 1 year ago |
Florent Daigniere | 6137f93d23 | 1 year ago |
Florent Daigniere | 3cb87b6e49 | 1 year ago |
Florent Daigniere | e3b875aa6b | 1 year ago |
Florent Daigniere | 3b5b00d87d | 1 year ago |
Florent Daigniere | e79d7fed55 | 1 year ago |
Florent Daigniere | 699be6f9fa | 1 year ago |
Florent Daigniere | 42cd5bf2dc | 1 year ago |
Florent Daigniere | 80559ecb71 | 1 year ago |
Florent Daigniere | 21b9f76ebc | 1 year ago |
Florent Daigniere | e5a1a353db | 1 year ago |
Florent Daigniere | 86637f0259 | 1 year ago |
bors[bot] | 68bb8da2b7 | 1 year ago |
Florent Daigniere | 7745420fe0 | 1 year ago |
bors[bot] | b66f3fe9de | 1 year ago |
Florent Daigniere | b9b0c77d2e | 1 year ago |
Florent Daigniere | 15b889fac8 | 1 year ago |
bors[bot] | f43c8c652e | 1 year ago |
Dimitri Huisman | 8afb544a10 | 1 year ago |
Florent Daigniere | 32f3241569 | 1 year ago |
Florent Daigniere | 7ab3d8f9fe | 1 year ago |
Florent Daigniere | aa44a42654 | 1 year ago |
Florent Daigniere | 04f6bd2633 | 1 year ago |
Florent Daigniere | d43e7f72df | 1 year ago |
Florent Daigniere | 1f895d5f82 | 1 year ago |
Florent Daigniere | 031a157ad9 | 1 year ago |
bors[bot] | 04a196c417 | 1 year ago |
bors[bot] | 40bdf7a6d9 | 1 year ago |
bors[bot] | 3b150ff9a4 | 1 year ago |
Florent Daigniere | b9e5560fb6 | 1 year ago |
Florent Daigniere | 63513608b9 | 1 year ago |
Florent Daigniere | 66de1dcec8 | 1 year ago |
Florent Daigniere | 81628149a2 | 1 year ago |
Florent Daigniere | 9b2f018be6 | 1 year ago |
Florent Daigniere | 76f8517e00 | 1 year ago |
Florent Daigniere | b9564c0bc9 | 1 year ago |
Florent Daigniere | 19af2944d7 | 1 year ago |
Alexander Graf | 6b470ac403 | 1 year ago |
Florent Daigniere | 7aad1158fb | 1 year ago |
Florent Daigniere | a566cb07d6 | 1 year ago |
Florent Daigniere | 08b3a2814b | 1 year ago |
Florent Daigniere | 385b6ac85d | 1 year ago |
Florent Daigniere | 6474108056 | 1 year ago |
Florent Daigniere | fb75cca2f4 | 1 year ago |
Florent Daigniere | c0c91691fd | 1 year ago |
bors[bot] | d8e2a2960b | 1 year ago |
Alexander Graf | b0b64a8e63 | 1 year ago |
Florent Daigniere | 505bb79a78 | 1 year ago |
Florent Daigniere | 9c7dfbeb24 | 1 year ago |
Florent Daigniere | 08a9ab9a56 | 1 year ago |
Florent Daigniere | 455180043d | 1 year ago |
Florent Daigniere | 56a106ad60 | 1 year ago |
Florent Daigniere | 071ad15a97 | 1 year ago |
Florent Daigniere | 6b2cb95a7d | 1 year ago |
Florent Daigniere | a508eeaafb | 1 year ago |
Florent Daigniere | f2f430af5d | 1 year ago |
Florent Daigniere | 06c0c78956 | 1 year ago |
Florent Daigniere | d7b80e94a4 | 1 year ago |
Florent Daigniere | 7ebac75045 | 1 year ago |
Florent Daigniere | f3a91d1a18 | 1 year ago |
Florent Daigniere | b488e57602 | 1 year ago |
Florent Daigniere | 225322fe88 | 1 year ago |
Florent Daigniere | ad17b10c8e | 1 year ago |
Florent Daigniere | 4517ce23a6 | 1 year ago |
Florent Daigniere | 6d8cc9083b | 1 year ago |
Florent Daigniere | 729838c8fe | 1 year ago |
Florent Daigniere | 1379a58352 | 1 year ago |
Florent Daigniere | 50f94a282f | 1 year ago |
Florent Daigniere | 710dde1faf | 1 year ago |
Florent Daigniere | 7e722cd0c3 | 1 year ago |
Florent Daigniere | 224f2f4508 | 1 year ago |
Florent Daigniere | a8d405cb48 | 1 year ago |
Florent Daigniere | ae64c6cc30 | 1 year ago |
Florent Daigniere | 13adf4aeec | 1 year ago |
Florent Daigniere | 1edef755f1 | 1 year ago |
Florent Daigniere | dc9e2a3e70 | 1 year ago |
bors[bot] | 8a90f83bd0 | 1 year ago |
Florent Daigniere | f11c451403 | 1 year ago |
Florent Daigniere | 97df65e9ef | 1 year ago |
bors[bot] | 8d392e8056 | 1 year ago |
Dimitri Huisman | 0e5443a867 | 1 year ago |
Dimitri Huisman | 59c5b152b2 | 1 year ago |
Dimitri Huisman | f6cdfb3392 | 1 year ago |
Dimitri Huisman | 2a894cb15d | 2 years ago |
Dimitri Huisman | 92f270c94e | 2 years ago |
bors[bot] | 745c211c4a | 2 years ago |
bors[bot] | 0839490beb | 2 years ago |
Florent Daigniere | c91c9df134 | 2 years ago |
bors[bot] | cf6da1492e | 2 years ago |
Vincent Kling | 728afdd34a | 2 years ago |
Alexander Graf | e0d2432c6b | 2 years ago |
Alexander Graf | 2a4402cdc2 | 2 years ago |
Alexander Graf | af6cf5fd1d | 2 years ago |
Alexander Graf | 2778641e78 | 2 years ago |
Alexander Graf | 4776094ea7 | 2 years ago |
Alexander Graf | 6218b36372 | 2 years ago |
Alexander Graf | 1ae9156756 | 2 years ago |
Alexander Graf | a74396a9ef | 2 years ago |
Alexander Graf | 047413185e | 2 years ago |
Alexander Graf | 7e36694b64 | 2 years ago |
Vincent Kling | 4a74cd9afe | 2 years ago |
Vincent Kling | 6901b0f05e | 2 years ago |
bors[bot] | 896e7fb54b | 2 years ago |
Alexander Graf | 4b179d9008 | 2 years ago |
bors[bot] | 4563038b32 | 2 years ago |
Alexander Graf | 36019a8ce9 | 2 years ago |
Alexander Graf | dd3cd1263e | 2 years ago |
Alexander Graf | 91e12d510d | 2 years ago |
Alexander Graf | defd533319 | 2 years ago |
Alexander Graf | db87a0f3a1 | 2 years ago |
Alexander Graf | f7caaddbec | 2 years ago |
Alexander Graf | 71263f1a8c | 2 years ago |
Alexander Graf | fd8570ec34 | 2 years ago |
Alexander Graf | bbeb211d72 | 2 years ago |
Alexander Graf | 1d90dc3ea3 | 2 years ago |
Alexander Graf | c507b765be | 2 years ago |
Alexander Graf | 8732b70b30 | 2 years ago |
Alexander Graf | ea636a1835 | 2 years ago |
bors[bot] | ac93e6a9be | 2 years ago |
Dimitri Huisman | 2a3266b6b8 | 2 years ago |
Dimitri Huisman | b2e47642f7 | 2 years ago |
Alexander Graf | 311f41c331 | 2 years ago |
Alexander Graf | 27a5f9db65 | 2 years ago |
Vincent Kling | 83fdc07a6f | 2 years ago |
Florent Daigniere | 3e9def6cd9 | 2 years ago |
Florent Daigniere | 54e9858633 | 2 years ago |
Florent Daigniere | 14f802fb4a | 2 years ago |
bors[bot] | e0ff135a00 | 2 years ago |
Alexander Graf | c57706ad27 | 2 years ago |
Alexander Graf | 46773f639b | 2 years ago |
Alexander Graf | 595b32cf97 | 2 years ago |
Alexander Graf | bec0b1c3b2 | 2 years ago |
Florent Daigniere | 001acd60ac | 2 years ago |
Alexander Graf | dec5309ef9 | 2 years ago |
Florent Daigniere | 6b7026ef69 | 2 years ago |
Florent Daigniere | 24b2c7c04a | 2 years ago |
Florent Daigniere | 66250e396c | 2 years ago |
bors[bot] | 5b2b379c91 | 2 years ago |
wkr | d920b3d037 | 2 years ago |
bors[bot] | 323f0a4e70 | 2 years ago |
Dimitri Huisman | db7ce8c83e | 2 years ago |
bors[bot] | 4b1143550d | 2 years ago |
Dimitri Huisman | b3151e9904 | 2 years ago |
bors[bot] | c6deb84ab0 | 2 years ago |
Florent Daigniere | ff9f152a52 | 2 years ago |
Florent Daigniere | 5137b235e9 | 2 years ago |
bors[bot] | 4d8585a3fe | 2 years ago |
Dimitri Huisman | 6549dbf247 | 2 years ago |
Dimitri Huisman | c7cba1b075 | 2 years ago |
bors[bot] | 0015335f4a | 2 years ago |
Alexander Graf | a2d43be6de | 2 years ago |
bors[bot] | 32edfce12b | 2 years ago |
Dimitri Huisman | e915e444e9 | 2 years ago |
bors[bot] | 659cf8894c | 2 years ago |
bors[bot] | 0618fbb472 | 2 years ago |
Alexander Graf | 91f86a4c2a | 2 years ago |
Alexander Graf | bba98b320e | 2 years ago |
Florent Daigniere | 5d314c49ae | 2 years ago |
Florent Daigniere | 9cb8df57c6 | 2 years ago |
Florent Daigniere | afbaabd8cd | 2 years ago |
Florent Daigniere | 6def1b555b | 2 years ago |
Florent Daigniere | c1f571a4c3 | 2 years ago |
Florent Daigniere | 96d9289630 | 2 years ago |
Florent Daigniere | cdc9b63a46 | 2 years ago |
Florent Daigniere | 2a417dbfc2 | 2 years ago |
Florent Daigniere | 1ce889b91b | 2 years ago |
Florent Daigniere | e10527a4bf | 2 years ago |
Florent Daigniere | 1ae4c37cb9 | 2 years ago |
Florent Daigniere | 5ec4277e1e | 2 years ago |
Florent Daigniere | cf34be967c | 2 years ago |
bors[bot] | 62c919da09 | 2 years ago |
Florent Daigniere | 340e359096 | 2 years ago |
Florent Daigniere | 076d67b513 | 2 years ago |
Florent Daigniere | 2e467092a2 | 2 years ago |
bors[bot] | 12480ccbff | 2 years ago |
Florent Daigniere | a63bad6bf2 | 2 years ago |
Florent Daigniere | 8942448561 | 2 years ago |
Dimitri Huisman | 06b784da57 | 2 years ago |
bors[bot] | 9975a793fe | 2 years ago |
Florent Daigniere | ec4224123b | 2 years ago |
bors[bot] | 5703e97c73 | 2 years ago |
Alexander Graf | 024b0573b3 | 2 years ago |
Dimitri Huisman | 4be0cbf4da | 2 years ago |
Vincent Kling | 6363acf30a | 2 years ago |
Vincent Kling | 6b785abb01 | 2 years ago |
bors[bot] | 7e29248980 | 2 years ago |
Alexander Graf | 005a8fa1fc | 2 years ago |
Florent Daigniere | 89f7d983b4 | 2 years ago |
Florent Daigniere | d8cf0c3848 | 2 years ago |
Florent Daigniere | 0f17299b4e | 2 years ago |
Florent Daigniere | 95a3a3d342 | 2 years ago |
Florent Daigniere | 84a722eabc | 2 years ago |
Florent Daigniere | 07bf8ce6df | 2 years ago |
Florent Daigniere | bd1b73032c | 2 years ago |
bors[bot] | 9d5d55f969 | 2 years ago |
Blaž Zupan | 56617bbe12 | 2 years ago |
Florent Daigniere | c4fcaed7d4 | 2 years ago |
Vincent Kling | 8a60b658b4 | 2 years ago |
Florent Daigniere | 8929f54de5 | 2 years ago |
Florent Daigniere | 8da6117bb9 | 2 years ago |
Florent Daigniere | af87456faf | 2 years ago |
bors[bot] | bbbed4d9ac | 2 years ago |
Vincent Kling | 23d06a5761 | 2 years ago |
Florent Daigniere | be4dd6d84a | 2 years ago |
Florent Daigniere | f7b3aad831 | 2 years ago |
Dimitri Huisman | 6ea2d84a3c | 2 years ago |
Florent Daigniere | 0204c9e59d | 2 years ago |
Florent Daigniere | cc2c308d1d | 2 years ago |
Florent Daigniere | 8775a2bf04 | 2 years ago |
Dimitri Huisman | 451738e32b | 2 years ago |
Dimitri Huisman | f9ba0e688f | 2 years ago |
Dimitri Huisman | 92cb8c146b | 2 years ago |
Florent Daigniere | 5ebcecf4dd | 2 years ago |
Florent Daigniere | 3e51d15b03 | 2 years ago |
Alexander Graf | d9bf6875e1 | 2 years ago |
Alexander Graf | 7441a420c4 | 2 years ago |
Alexander Graf | 5c31120895 | 2 years ago |
Alexander Graf | 146921f619 | 2 years ago |
Alexander Graf | 4c1071a497 | 2 years ago |
Alexander Graf | a29f066858 | 2 years ago |
Alexander Graf | 52dd09d452 | 2 years ago |
Alexander Graf | 768c0cc1ce | 2 years ago |
Alexander Graf | 8668b269cd | 2 years ago |
Alexander Graf | 9f511faf64 | 2 years ago |
Dimitri Huisman | b711f930ef | 2 years ago |
Dimitri Huisman | c0066abd01 | 2 years ago |
kaiyou | f63837b8e1 | 2 years ago |
kaiyou | 68d44201ab | 2 years ago |
kaiyou | b198fde756 | 2 years ago |
kaiyou | 7f6d51904b | 2 years ago |
kaiyou | ef344c62f6 | 2 years ago |
kaiyou | 74a3e87de3 | 2 years ago |
kaiyou | 351b05b92d | 2 years ago |
kaiyou | 0370b26f3e | 2 years ago |
Alexander Graf | ce9d886195 | 2 years ago |
kaiyou | dbec5f0a6c | 2 years ago |
kaiyou | 3d0d831c76 | 2 years ago |
kaiyou | e2979f9103 | 2 years ago |
kaiyou | 6fadd39aea | 2 years ago |
kaiyou | 080e76f972 | 2 years ago |
kaiyou | 23e5aa2e05 | 2 years ago |
kaiyou | 814bb1f36d | 2 years ago |
kaiyou | d2b98ae323 | 2 years ago |
kaiyou | 81d171f978 | 2 years ago |
Pierre Jaury | d640da8787 | 2 years ago |
Pierre Jaury | c5fa0280a0 | 2 years ago |
Pierre Jaury | eb6b1866f1 | 2 years ago |
Pierre Jaury | b1b0aeb69d | 2 years ago |
Alexander Graf | b501498401 | 2 years ago |
Alexander Graf | 9fe452e3d1 | 2 years ago |
Alexander Graf | 5e552bae69 | 2 years ago |
Alexander Graf | 295d7ea675 | 2 years ago |
Vincent Kling | bda404182f | 2 years ago |
Vincent Kling | 10583f57dd | 2 years ago |
Vincent Kling | 102d96bc7d | 2 years ago |
Dimitri Huisman | 81c9e01d24 | 2 years ago |
enginefeeder101 | 82860d0f80 | 2 years ago |
enginefeeder101 | 4da0ff1856 | 2 years ago |
enginefeeder101 | 6c83d25312 | 2 years ago |
@ -1,101 +0,0 @@
|
||||
name: start-linux-arm
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '1.9'
|
||||
- master
|
||||
|
||||
concurrency: ci-arm-${{ github.ref }}
|
||||
|
||||
###############################################
|
||||
# REQUIRED secrets
|
||||
# ${{ secrets.Docker_Login }}
|
||||
# Username of docker login for pushing the images to repo env.DOCKER_ORG and env.DOCKER_ORG_TESTS
|
||||
# ${{ secrets.Docker_Password }}
|
||||
# Password of docker login for pushing the images to repo env.DOCKER_ORG and env.DOCKER_ORG_TESTS
|
||||
# Add the above secrets to your github repo to determine where the images will be pushed.
|
||||
################################################
|
||||
# REQUIRED global variables
|
||||
# DOCKER_ORG, docker org used for pushing release images (branch x.y and master)
|
||||
# DOCKER_ORG_TEST, docker org used for pushing images for testing (branch testing).
|
||||
env:
|
||||
DOCKER_ORG: mailu
|
||||
DOCKER_ORG_TEST: mailuci
|
||||
|
||||
jobs:
|
||||
# This job calculates all global job variables that are required by all the subsequent jobs.
|
||||
# All subsequent jobs will retrieve and use these variables. This way the variables only have to be derived once.
|
||||
derive-variables:
|
||||
name: derive variables
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
MAILU_VERSION: ${{ env.MAILU_VERSION }}
|
||||
PINNED_MAILU_VERSION: ${{ env.PINNED_MAILU_VERSION }}
|
||||
DOCKER_ORG: ${{ env.DOCKER_ORG_DERIVED }}
|
||||
BRANCH: ${{ env.BRANCH }}
|
||||
DEPLOY: ${{ env.DEPLOY }}
|
||||
RELEASE: ${{ env.RELEASE }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# fetch-depth 0 is required to also retrieve all tags.
|
||||
fetch-depth: 0
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: |
|
||||
echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
|
||||
#For branch TESTING, we set the image tag to pr-xxxx
|
||||
- name: Derive MAILU_VERSION and DEPLOY/RELEASE for other branches than testing
|
||||
if: env.BRANCH != 'testing'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "MAILU_VERSION=${{ env.BRANCH }}" >> $GITHUB_ENV
|
||||
echo "DOCKER_ORG_DERIVED=${{ env.DOCKER_ORG }}" >> $GITHUB_ENV
|
||||
echo "DEPLOY=true" >> $GITHUB_ENV
|
||||
echo "RELEASE=false" >> $GITHUB_ENV
|
||||
- name: Derive PINNED_MAILU_VERSION and DEPLOY/RELEASE for normal release x.y
|
||||
if: env.BRANCH != 'testing' && env.BRANCH != 'staging' && env.BRANCH != 'master'
|
||||
shell: bash
|
||||
run: |
|
||||
version=$( git tag --sort=version:refname --list "${{ env.MAILU_VERSION }}.*" | tail -1 );root_version=${version%.*};patch_version=${version##*.};if [ "$patch_version" == "" ]; then pinned_version=${{ env.MAILU_VERSION }}.0; else pinned_version=$root_version.$(expr $patch_version + 1); fi;echo "PINNED_MAILU_VERSION=$pinned_version" >> $GITHUB_ENV
|
||||
echo "RELEASE=true" >> $GITHUB_ENV
|
||||
echo "DEPLOY=true" >> $GITHUB_ENV
|
||||
echo "RELEASE=true" >> $GITHUB_ENV
|
||||
- name: Derive PINNED_MAILU_VERSION for staging for master
|
||||
if: env.BRANCH == 'master'
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_SHA: ${{ env.GITHUB_SHA }}
|
||||
run: |
|
||||
echo "PINNED_MAILU_VERSION=$GITHUB_SHA" >> $GITHUB_ENV
|
||||
echo "DEPLOY=true" >> $GITHUB_ENV
|
||||
echo "RELEASE=false" >> $GITHUB_ENV
|
||||
|
||||
build-test-deploy:
|
||||
needs:
|
||||
- derive-variables
|
||||
uses: ./.github/workflows/build_test_deploy.yml
|
||||
with:
|
||||
architecture: 'linux/arm64,linux/arm/v7'
|
||||
mailu_version: ${{needs.derive-variables.outputs.MAILU_VERSION}}-arm
|
||||
pinned_mailu_version: ${{needs.derive-variables.outputs.PINNED_MAILU_VERSION}}-arm
|
||||
docker_org: ${{needs.derive-variables.outputs.DOCKER_ORG}}
|
||||
branch: ${{needs.derive-variables.outputs.BRANCH}}
|
||||
deploy: ${{needs.derive-variables.outputs.DEPLOY}}
|
||||
release: ${{needs.derive-variables.outputs.RELEASE}}
|
||||
secrets: inherit
|
||||
|
||||
################################################
|
||||
# Code block that is used as one liner for the step:
|
||||
# Derive PINNED_MAILU_VERSION and DEPLOY/RELEASE for normal release x.y
|
||||
##!/bin/bash
|
||||
#version=$( git tag --sort=version:refname --list "{{ env.MAILU_VERSION }}.*" | tail -1 )
|
||||
#root_version=${version%.*}
|
||||
#patch_version=${version##*.}
|
||||
#if [ "$patch_version" == "" ]
|
||||
#then
|
||||
# pinned_version={{ env.MAILU_VERSION }}.0
|
||||
#else
|
||||
# pinned_version=$root_version.$(expr $patch_version + 1)
|
||||
#fi
|
||||
#echo "PINNED_MAILU_VERSION=$pinned_version" >> $GITHUB_ENV
|
@ -0,0 +1,22 @@
|
||||
# syntax=docker/dockerfile-upstream:1.4.3
|
||||
|
||||
FROM node:16-alpine3.16
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
COPY package.json ./
|
||||
|
||||
RUN set -euxo pipefail \
|
||||
; npm config set update-notifier false \
|
||||
; npm install --no-audit --no-fund \
|
||||
; sed -i 's/#007bff/#55a5d9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \
|
||||
; mkdir assets \
|
||||
; for l in ca da de:de-DE en:en-GB es:es-ES eu fr:fr-FR he hu is it:it-IT ja nb_NO:no-NB nl:nl-NL pl pt:pt-PT ru sv:sv-SE zh; do \
|
||||
cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \
|
||||
done
|
||||
|
||||
COPY assets/ ./assets/
|
||||
COPY webpack.config.js ./
|
||||
|
||||
RUN set -euxo pipefail \
|
||||
; node_modules/.bin/webpack-cli --color
|
@ -1,79 +0,0 @@
|
||||
require('./app.css');
|
||||
|
||||
import logo from './mailu.png';
|
||||
import modules from "./*.json";
|
||||
|
||||
// TODO: conditionally (or lazy) load select2 and dataTable
|
||||
$('document').ready(function() {
|
||||
|
||||
// intercept anchors with data-clicked attribute and open alternate location instead
|
||||
$('[data-clicked]').click(function(e) {
|
||||
e.preventDefault();
|
||||
window.location.href = $(this).data('clicked');
|
||||
});
|
||||
|
||||
// use post for language selection
|
||||
$('#mailu-languages > a').click(function(e) {
|
||||
e.preventDefault();
|
||||
$.post({
|
||||
url: $(this).attr('href'),
|
||||
success: function() {
|
||||
window.location = window.location.href;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// allow en-/disabling of inputs in fieldset with checkbox in legend
|
||||
$('fieldset legend input[type=checkbox]').change(function() {
|
||||
var fieldset = $(this).parents('fieldset');
|
||||
if (this.checked) {
|
||||
fieldset.removeAttr('disabled');
|
||||
fieldset.find('input,textarea').not(this).removeAttr('disabled');
|
||||
} else {
|
||||
fieldset.attr('disabled', '');
|
||||
fieldset.find('input,textarea').not(this).attr('disabled', '');
|
||||
}
|
||||
});
|
||||
|
||||
// display of range input value
|
||||
$('input[type=range]').each(function() {
|
||||
var value_element = $('#'+this.id+'_value');
|
||||
if (value_element.length) {
|
||||
value_element = $(value_element[0]);
|
||||
var infinity = $(this).data('infinity');
|
||||
var step = $(this).attr('step');
|
||||
$(this).on('input', function() {
|
||||
var num = (infinity && this.value == 0) ? '∞' : (this.value/step).toFixed(2);
|
||||
if (num.endsWith('.00')) num = num.substr(0, num.length - 3);
|
||||
value_element.text(num);
|
||||
}).trigger('input');
|
||||
}
|
||||
});
|
||||
|
||||
// init select2
|
||||
$('.mailselect').select2({
|
||||
tags: true,
|
||||
tokenSeparators: [',', ' '],
|
||||
});
|
||||
|
||||
// init dataTable
|
||||
var d = $(document.documentElement);
|
||||
$('.dataTable').DataTable({
|
||||
'responsive': true,
|
||||
language: {
|
||||
url: d.data('static') + d.attr('lang') + '.json',
|
||||
},
|
||||
});
|
||||
|
||||
// init clipboard.js
|
||||
new ClipboardJS('.btn-clip');
|
||||
|
||||
// disable login if not possible
|
||||
var l = $('#login_needs_https');
|
||||
if (l.length && window.location.protocol != 'https:') {
|
||||
l.removeClass("d-none");
|
||||
$('form :input').prop('disabled', true);
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -0,0 +1,139 @@
|
||||
// Inspired from https://github.com/mehdibo/hibp-js/blob/master/hibp.js
|
||||
function sha1(string) {
|
||||
var buffer = new TextEncoder("utf-8").encode(string);
|
||||
return crypto.subtle.digest("SHA-1", buffer).then(function (buffer) {
|
||||
// Get the hex code
|
||||
var hexCodes = [];
|
||||
var view = new DataView(buffer);
|
||||
for (var i = 0; i < view.byteLength; i += 4) {
|
||||
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
|
||||
var value = view.getUint32(i);
|
||||
// toString(16) will give the hex representation of the number without padding
|
||||
var stringValue = value.toString(16);
|
||||
// We use concatenation and slice for padding
|
||||
var padding = '00000000';
|
||||
var paddedValue = (padding + stringValue).slice(-padding.length);
|
||||
hexCodes.push(paddedValue);
|
||||
}
|
||||
// Join all the hex strings into one
|
||||
return hexCodes.join("");
|
||||
});
|
||||
}
|
||||
|
||||
function hibpCheck(pwd) {
|
||||
// We hash the pwd first
|
||||
sha1(pwd).then(function(hash){
|
||||
// We send the first 5 chars of the hash to hibp's API
|
||||
const req = new XMLHttpRequest();
|
||||
req.open('GET', 'https://api.pwnedpasswords.com/range/'+hash.substr(0, 5));
|
||||
req.setRequestHeader('Add-Padding', 'true');
|
||||
req.addEventListener("load", function(){
|
||||
// When we get back a response from the server
|
||||
// We create an array of lines and loop through them
|
||||
const lines = this.responseText.split("\n");
|
||||
const hashSub = hash.slice(5).toUpperCase();
|
||||
for (var i in lines){
|
||||
// Check if the line matches the rest of the hash
|
||||
if (lines[i].substring(0, 35) == hashSub){
|
||||
const val = parseInt(lines[i].trimEnd("\r").split(":")[1]);
|
||||
if (val > 0) {
|
||||
$("#pwned").val(val);
|
||||
}
|
||||
return; // If found no need to continue the loop
|
||||
}
|
||||
}
|
||||
$("#pwned").val(0);
|
||||
});
|
||||
req.send();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: conditionally (or lazy) load select2 and dataTable
|
||||
$('document').ready(function() {
|
||||
|
||||
// intercept anchors with data-clicked attribute and open alternate location instead
|
||||
$('[data-clicked]').click(function(e) {
|
||||
e.preventDefault();
|
||||
window.location.href = $(this).data('clicked');
|
||||
});
|
||||
|
||||
// use post for language selection
|
||||
$('#mailu-languages > a').click(function(e) {
|
||||
e.preventDefault();
|
||||
$.post({
|
||||
url: $(this).attr('href'),
|
||||
success: function() {
|
||||
window.location = window.location.href;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// allow en-/disabling of inputs in fieldset with checkbox in legend
|
||||
$('fieldset legend input[type=checkbox]').change(function() {
|
||||
var fieldset = $(this).parents('fieldset');
|
||||
if (this.checked) {
|
||||
fieldset.removeAttr('disabled');
|
||||
fieldset.find('input,textarea').not(this).removeAttr('disabled');
|
||||
} else {
|
||||
fieldset.attr('disabled', '');
|
||||
fieldset.find('input,textarea').not(this).attr('disabled', '');
|
||||
}
|
||||
});
|
||||
|
||||
// display of range input value
|
||||
$('input[type=range]').each(function() {
|
||||
var value_element = $('#'+this.id+'_value');
|
||||
if (value_element.length) {
|
||||
value_element = $(value_element[0]);
|
||||
var infinity = $(this).data('infinity');
|
||||
var unit = $(this).data('unit');
|
||||
if (typeof unit === 'undefined' || unit === false) {
|
||||
unit=1;
|
||||
}
|
||||
$(this).on('input', function() {
|
||||
var num = (infinity && this.value == 0) ? '∞' : (this.value/unit).toFixed(2);
|
||||
if (num.endsWith('.00')) num = num.substr(0, num.length - 3);
|
||||
value_element.text(num);
|
||||
}).trigger('input');
|
||||
}
|
||||
});
|
||||
|
||||
// init select2
|
||||
$('.mailselect').select2({
|
||||
tags: true,
|
||||
tokenSeparators: [',', ' '],
|
||||
});
|
||||
|
||||
// init dataTable
|
||||
var d = $(document.documentElement);
|
||||
$('.dataTable').DataTable({
|
||||
'responsive': true,
|
||||
language: {
|
||||
url: d.data('static') + d.attr('lang') + '.json',
|
||||
},
|
||||
});
|
||||
|
||||
// init clipboard.js
|
||||
new ClipboardJS('.btn-clip');
|
||||
|
||||
// disable login if not possible
|
||||
var l = $('#login_needs_https');
|
||||
if (l.length && window.location.protocol != 'https:') {
|
||||
l.removeClass("d-none");
|
||||
$('form :input').prop('disabled', true);
|
||||
}
|
||||
|
||||
if (window.isSecureContext) {
|
||||
$("#pw").on("change paste", function(){
|
||||
hibpCheck($(this).val());
|
||||
return true;
|
||||
});
|
||||
$("#pw").closest("form").submit(function(event){
|
||||
if (parseInt($("#pwned").val()) < 0) {
|
||||
hibpCheck($("#pw").val());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
@ -0,0 +1,32 @@
|
||||
from flask import redirect, url_for, Blueprint
|
||||
from flask_restx import apidoc
|
||||
from . import v1 as APIv1
|
||||
|
||||
def register(app, web_api_root):
|
||||
|
||||
APIv1.app = app
|
||||
# register api bluprint(s)
|
||||
apidoc.apidoc.url_prefix = f'{web_api_root}/v{int(APIv1.VERSION)}'
|
||||
APIv1.api_token = app.config['API_TOKEN']
|
||||
if app.config['API_TOKEN'] != '':
|
||||
app.register_blueprint(APIv1.blueprint, url_prefix=f'{web_api_root}/v{int(APIv1.VERSION)}')
|
||||
|
||||
# add redirect to current api version
|
||||
redirect_api = Blueprint('redirect_api', __name__)
|
||||
@redirect_api.route('/')
|
||||
def redir():
|
||||
return redirect(url_for(f'{APIv1.blueprint.name}.root'))
|
||||
app.register_blueprint(redirect_api, url_prefix=f'{web_api_root}')
|
||||
|
||||
# swagger ui config
|
||||
app.config.SWAGGER_UI_DOC_EXPANSION = 'list'
|
||||
app.config.SWAGGER_UI_OPERATION_ID = True
|
||||
app.config.SWAGGER_UI_REQUEST_DURATION = True
|
||||
app.config.RESTX_MASK_SWAGGER = False
|
||||
else:
|
||||
api = Blueprint('api', __name__)
|
||||
@api.route('/', defaults={'path': ''})
|
||||
@api.route('/<path:path>')
|
||||
def api_token_missing(path):
|
||||
return "<p>Error: API_TOKEN is not configured</p>", 500
|
||||
app.register_blueprint(api, url_prefix=f'{web_api_root}')
|
@ -0,0 +1,42 @@
|
||||
from .. import models, utils
|
||||
from . import v1
|
||||
from flask import request
|
||||
import flask
|
||||
import hmac
|
||||
from functools import wraps
|
||||
from flask_restx import abort
|
||||
from sqlalchemy.sql.expression import label
|
||||
|
||||
def fqdn_in_use(name):
|
||||
d = models.db.session.query(label('name', models.Domain.name))
|
||||
a = models.db.session.query(label('name', models.Alternative.name))
|
||||
r = models.db.session.query(label('name', models.Relay.name))
|
||||
u = d.union_all(a).union_all(r).filter_by(name=name)
|
||||
if models.db.session.query(u.exists()).scalar():
|
||||
return True
|
||||
return False
|
||||
|
||||
""" Decorator for validating api token for authentication """
|
||||
def api_token_authorization(func):
|
||||
@wraps(func)
|
||||
def decorated_function(*args, **kwds):
|
||||
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
|
||||
if utils.limiter.should_rate_limit_ip(client_ip):
|
||||
abort(429, 'Too many attempts from your IP (rate-limit)' )
|
||||
if not request.headers.get('Authorization'):
|
||||
abort(401, 'A valid Bearer token is expected which is provided as request header')
|
||||
#Client provides 'Authentication: Bearer <token>'
|
||||
if (' ' in request.headers.get('Authorization')
|
||||
and not hmac.compare_digest(request.headers.get('Authorization'), 'Bearer ' + v1.api_token)):
|
||||
utils.limiter.rate_limit_ip(client_ip)
|
||||
flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.')
|
||||
abort(403, 'A valid Bearer token is expected which is provided as request header')
|
||||
#Client provides 'Authentication: <token>'
|
||||
elif (' ' not in request.headers.get('Authorization')
|
||||
and not hmac.compare_digest(request.headers.get('Authorization'), v1.api_token)):
|
||||
utils.limiter.rate_limit_ip(client_ip)
|
||||
flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.')
|
||||
abort(403, 'A valid Bearer token is expected which is provided as request header')
|
||||
flask.current_app.logger.info(f'Valid API token provided by {client_ip}.')
|
||||
return func(*args, **kwds)
|
||||
return decorated_function
|
@ -0,0 +1,43 @@
|
||||
from flask import Blueprint
|
||||
from flask_restx import Api, fields
|
||||
|
||||
|
||||
VERSION = 1.0
|
||||
api_token = None
|
||||
|
||||
blueprint = Blueprint(f'api_v{int(VERSION)}', __name__)
|
||||
|
||||
authorization = {
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'in': 'header',
|
||||
'name': 'Authorization'
|
||||
}
|
||||
}
|
||||
|
||||
api = Api(
|
||||
blueprint, version=f'{VERSION:.1f}',
|
||||
title='Mailu API', default_label='Mailu',
|
||||
validate=True,
|
||||
authorizations=authorization,
|
||||
security='Bearer',
|
||||
doc='/'
|
||||
)
|
||||
|
||||
response_fields = api.model('Response', {
|
||||
'code': fields.Integer,
|
||||
'message': fields.String,
|
||||
})
|
||||
|
||||
error_fields = api.model('Error', {
|
||||
'errors': fields.Nested(api.model('Error_Key', {
|
||||
'key': fields.String,
|
||||
'message':fields.String
|
||||
})),
|
||||
'message': fields.String,
|
||||
})
|
||||
|
||||
from . import domains
|
||||
from . import alias
|
||||
from . import relay
|
||||
from . import user
|
@ -0,0 +1,126 @@
|
||||
from flask_restx import Resource, fields, marshal
|
||||
from . import api, response_fields
|
||||
from .. import common
|
||||
from ... import models
|
||||
|
||||
db = models.db
|
||||
|
||||
alias = api.namespace('alias', description='Alias operations')
|
||||
|
||||
alias_fields_update = alias.model('AliasUpdate', {
|
||||
'comment': fields.String(description='a comment'),
|
||||
'destination': fields.List(fields.String(description='alias email address', example='user@example.com')),
|
||||
'wildcard': fields.Boolean(description='enable SQL Like wildcard syntax')
|
||||
})
|
||||
|
||||
alias_fields = alias.inherit('Alias',alias_fields_update, {
|
||||
'email': fields.String(description='the alias email address', example='user@example.com', required=True),
|
||||
'destination': fields.List(fields.String(description='alias email address', example='user@example.com', required=True)),
|
||||
|
||||
})
|
||||
|
||||
|
||||
@alias.route('')
|
||||
class Aliases(Resource):
|
||||
@alias.doc('list_alias')
|
||||
@alias.marshal_with(alias_fields, as_list=True, skip_none=True, mask=None)
|
||||
@alias.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self):
|
||||
""" List aliases """
|
||||
return models.Alias.query.all()
|
||||
|
||||
@alias.doc('create_alias')
|
||||
@alias.expect(alias_fields)
|
||||
@alias.response(200, 'Success', response_fields)
|
||||
@alias.response(400, 'Input validation exception', response_fields)
|
||||
@alias.response(409, 'Duplicate alias', response_fields)
|
||||
@alias.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def post(self):
|
||||
""" Create a new alias """
|
||||
data = api.payload
|
||||
|
||||
alias_found = models.Alias.query.filter_by(email = data['email']).first()
|
||||
if alias_found:
|
||||
return { 'code': 409, 'message': f'Duplicate alias {data["email"]}'}, 409
|
||||
|
||||
alias_model = models.Alias(email=data["email"],destination=data['destination'])
|
||||
if 'comment' in data:
|
||||
alias_model.comment = data['comment']
|
||||
if 'wildcard' in data:
|
||||
alias_model.wildcard = data['wildcard']
|
||||
db.session.add(alias_model)
|
||||
db.session.commit()
|
||||
|
||||
return {'code': 200, 'message': f'Alias {data["email"]} to destination {data["destination"]} has been created'}, 200
|
||||
|
||||
@alias.route('/<string:alias>')
|
||||
class Alias(Resource):
|
||||
@alias.doc('find_alias')
|
||||
@alias.response(200, 'Success', alias_fields)
|
||||
@alias.response(404, 'Alias not found', response_fields)
|
||||
@alias.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, alias):
|
||||
""" Find alias """
|
||||
alias_found = models.Alias.query.filter_by(email = alias).first()
|
||||
if alias_found is None:
|
||||
return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404
|
||||
else:
|
||||
return marshal(alias_found,alias_fields), 200
|
||||
|
||||
@alias.doc('update_alias')
|
||||
@alias.expect(alias_fields_update)
|
||||
@alias.response(200, 'Success', response_fields)
|
||||
@alias.response(404, 'Alias not found', response_fields)
|
||||
@alias.response(400, 'Input validation exception', response_fields)
|
||||
@alias.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def patch(self, alias):
|
||||
""" Update alias """
|
||||
data = api.payload
|
||||
alias_found = models.Alias.query.filter_by(email = alias).first()
|
||||
if alias_found is None:
|
||||
return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404
|
||||
if 'comment' in data:
|
||||
alias_found.comment = data['comment']
|
||||
if 'destination' in data:
|
||||
alias_found.destination = data['destination']
|
||||
if 'wildcard' in data:
|
||||
alias_found.wildcard = data['wildcard']
|
||||
db.session.add(alias_found)
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Alias {alias} has been updated'}
|
||||
|
||||
@alias.doc('delete_alias')
|
||||
@alias.response(200, 'Success', response_fields)
|
||||
@alias.response(404, 'Alias not found', response_fields)
|
||||
@alias.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def delete(self, alias):
|
||||
""" Delete alias """
|
||||
alias_found = models.Alias.query.filter_by(email = alias).first()
|
||||
if alias_found is None:
|
||||
return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404
|
||||
db.session.delete(alias_found)
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Alias {alias} has been deleted'}, 200
|
||||
|
||||
@alias.route('/destination/<string:domain>')
|
||||
class AliasWithDest(Resource):
|
||||
@alias.doc('find_alias_filter_domain')
|
||||
@alias.response(200, 'Success', alias_fields)
|
||||
@alias.response(404, 'Alias or domain not found', response_fields)
|
||||
@alias.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, domain):
|
||||
""" Find aliases of domain """
|
||||
domain_found = models.Domain.query.filter_by(name=domain).first()
|
||||
if domain_found is None:
|
||||
return { 'code': 404, 'message': f'Domain {domain} cannot be found'}, 404
|
||||
aliases_found = domain_found.aliases
|
||||
if aliases_found.count == 0:
|
||||
return { 'code': 404, 'message': f'No alias can be found for domain {domain}'}, 404
|
||||
else:
|
||||
return marshal(aliases_found, alias_fields), 200
|
@ -0,0 +1,410 @@
|
||||
import validators
|
||||
from flask_restx import Resource, fields, marshal
|
||||
from . import api, response_fields, user
|
||||
from .. import common
|
||||
from ... import models
|
||||
|
||||
db = models.db
|
||||
|
||||
dom = api.namespace('domain', description='Domain operations')
|
||||
alt = api.namespace('alternative', description='Alternative operations')
|
||||
|
||||
domain_fields = api.model('Domain', {
|
||||
'name': fields.String(description='FQDN (e.g. example.com)', example='example.com', required=True),
|
||||
'comment': fields.String(description='a comment'),
|
||||
'max_users': fields.Integer(description='maximum number of users', min=-1, default=-1),
|
||||
'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1),
|
||||
'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0),
|
||||
'signup_enabled': fields.Boolean(description='allow signup'),
|
||||
'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')),
|
||||
})
|
||||
|
||||
domain_fields_update = api.model('DomainUpdate', {
|
||||
'comment': fields.String(description='a comment'),
|
||||
'max_users': fields.Integer(description='maximum number of users', min=-1, default=-1),
|
||||
'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1),
|
||||
'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0),
|
||||
'signup_enabled': fields.Boolean(description='allow signup'),
|
||||
'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')),
|
||||
})
|
||||
|
||||
domain_fields_get = api.model('DomainGet', {
|
||||
'name': fields.String(description='FQDN (e.g. example.com)', example='example.com', required=True),
|
||||
'comment': fields.String(description='a comment'),
|
||||
'max_users': fields.Integer(description='maximum number of users', min=-1, default=-1),
|
||||
'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1),
|
||||
'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0),
|
||||
'signup_enabled': fields.Boolean(description='allow signup'),
|
||||
'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')),
|
||||
'dns_autoconfig': fields.List(fields.String(description='DNS client auto-configuration entry')),
|
||||
'dns_mx': fields.String(Description='MX record for domain'),
|
||||
'dns_spf': fields.String(Description='SPF record for domain'),
|
||||
'dns_dkim': fields.String(Description='DKIM record for domain'),
|
||||
'dns_dmarc': fields.String(Description='DMARC record for domain'),
|
||||
'dns_dmarc_report': fields.String(Description='DMARC report record for domain'),
|
||||
'dns_tlsa': fields.String(Description='TLSA record for domain'),
|
||||
})
|
||||
|
||||
domain_fields_dns = api.model('DomainDNS', {
|
||||
'dns_autoconfig': fields.List(fields.String(description='DNS client auto-configuration entry')),
|
||||
'dns_mx': fields.String(Description='MX record for domain'),
|
||||
'dns_spf': fields.String(Description='SPF record for domain'),
|
||||
'dns_dkim': fields.String(Description='DKIM record for domain'),
|
||||
'dns_dmarc': fields.String(Description='DMARC record for domain'),
|
||||
'dns_dmarc_report': fields.String(Description='DMARC report record for domain'),
|
||||
'dns_tlsa': fields.String(Description='TLSA record for domain'),
|
||||
})
|
||||
|
||||
manager_fields = api.model('Manager', {
|
||||
'domain_name': fields.String(description='domain managed by manager'),
|
||||
'user_email': fields.String(description='email address of manager'),
|
||||
})
|
||||
|
||||
manager_fields_create = api.model('ManagerCreate', {
|
||||
'user_email': fields.String(description='email address of manager', required=True),
|
||||
})
|
||||
|
||||
alternative_fields_update = api.model('AlternativeDomainUpdate', {
|
||||
'domain': fields.String(description='domain FQDN', example='example.com', required=False),
|
||||
})
|
||||
|
||||
alternative_fields = api.model('AlternativeDomain', {
|
||||
'name': fields.String(description='alternative FQDN', example='example2.com', required=True),
|
||||
'domain': fields.String(description='domain FQDN', example='example.com', required=True),
|
||||
})
|
||||
|
||||
|
||||
@dom.route('')
|
||||
class Domains(Resource):
|
||||
@dom.doc('list_domain')
|
||||
@dom.marshal_with(domain_fields_get, as_list=True, skip_none=True, mask=None)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self):
|
||||
""" List domains """
|
||||
return models.Domain.query.all()
|
||||
|
||||
@dom.doc('create_domain')
|
||||
@dom.expect(domain_fields)
|
||||
@dom.response(200, 'Success', response_fields)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(409, 'Duplicate domain/alternative name', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def post(self):
|
||||
""" Create a new domain """
|
||||
data = api.payload
|
||||
if not validators.domain(data['name']):
|
||||
return { 'code': 400, 'message': f'Domain {data["name"]} is not a valid domain'}, 400
|
||||
|
||||
if common.fqdn_in_use(data['name']):
|
||||
return { 'code': 409, 'message': f'Duplicate domain name {data["name"]}'}, 409
|
||||
if 'alternatives' in data:
|
||||
#check if duplicate alternatives are supplied
|
||||
if [x for x in data['alternatives'] if data['alternatives'].count(x) >= 2]:
|
||||
return { 'code': 409, 'message': f'Duplicate alternative domain names in request' }, 409
|
||||
for item in data['alternatives']:
|
||||
if common.fqdn_in_use(item):
|
||||
return { 'code': 409, 'message': f'Duplicate alternative domain name {item}' }, 409
|
||||
if not validators.domain(item):
|
||||
return { 'code': 400, 'message': f'Alternative domain {item} is not a valid domain'}, 400
|
||||
for item in data['alternatives']:
|
||||
alternative = models.Alternative(name=item, domain_name=data['name'])
|
||||
models.db.session.add(alternative)
|
||||
domain_new = models.Domain(name=data['name'])
|
||||
if 'comment' in data:
|
||||
domain_new.comment = data['comment']
|
||||
if 'max_users' in data:
|
||||
domain_new.comment = data['max_users']
|
||||
if 'max_aliases' in data:
|
||||
domain_new.comment = data['max_aliases']
|
||||
if 'max_quota_bytes' in data:
|
||||
domain_new.comment = data['max_quota_bytes']
|
||||
if 'signup_enabled' in data:
|
||||
domain_new.comment = data['signup_enabled']
|
||||
models.db.session.add(domain_new)
|
||||
#apply the changes
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Domain {data["name"]} has been created'}, 200
|
||||
|
||||
@dom.route('/<domain>')
|
||||
class Domain(Resource):
|
||||
|
||||
@dom.doc('find_domain')
|
||||
@dom.response(200, 'Success', domain_fields)
|
||||
@dom.response(404, 'Domain not found', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, domain):
|
||||
""" Find domain by name """
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain_found = models.Domain.query.get(domain)
|
||||
if not domain_found:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
return marshal(domain_found, domain_fields_get), 200
|
||||
|
||||
@dom.doc('update_domain')
|
||||
@dom.expect(domain_fields_update)
|
||||
@dom.response(200, 'Success', response_fields)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(404, 'Domain not found', response_fields)
|
||||
@dom.response(409, 'Duplicate domain/alternative name', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def patch(self, domain):
|
||||
""" Update an existing domain """
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain_found = models.Domain.query.get(domain)
|
||||
if not domain:
|
||||
return { 'code': 404, 'message': f'Domain {data["name"]} does not exist'}, 404
|
||||
data = api.payload
|
||||
|
||||
if 'alternatives' in data:
|
||||
#check if duplicate alternatives are supplied
|
||||
if [x for x in data['alternatives'] if data['alternatives'].count(x) >= 2]:
|
||||
return { 'code': 409, 'message': f'Duplicate alternative domain names in request' }, 409
|
||||
for item in data['alternatives']:
|
||||
if common.fqdn_in_use(item):
|
||||
return { 'code': 409, 'message': f'Duplicate alternative domain name {item}' }, 409
|
||||
if not validators.domain(item):
|
||||
return { 'code': 400, 'message': f'Alternative domain {item} is not a valid domain'}, 400
|
||||
for item in data['alternatives']:
|
||||
alternative = models.Alternative(name=item, domain_name=data['name'])
|
||||
models.db.session.add(alternative)
|
||||
|
||||
if 'comment' in data:
|
||||
domain_found.comment = data['comment']
|
||||
if 'max_users' in data:
|
||||
domain_found.comment = data['max_users']
|
||||
if 'max_aliases' in data:
|
||||
domain_found.comment = data['max_aliases']
|
||||
if 'max_quota_bytes' in data:
|
||||
domain_found.comment = data['max_quota_bytes']
|
||||
if 'signup_enabled' in data:
|
||||
domain_found.comment = data['signup_enabled']
|
||||
models.db.session.add(domain_found)
|
||||
|
||||
#apply the changes
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Domain {domain} has been updated'}, 200
|
||||
|
||||
@dom.doc('delete_domain')
|
||||
@dom.response(200, 'Success', response_fields)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(404, 'Domain not found', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def delete(self, domain):
|
||||
""" Delete domain """
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain_found = models.Domain.query.get(domain)
|
||||
if not domain:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
db.session.delete(domain_found)
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Domain {domain} has been deleted'}, 200
|
||||
|
||||
@dom.route('/<domain>/dkim')
|
||||
class Domain(Resource):
|
||||
@dom.doc('generate_dkim')
|
||||
@dom.response(200, 'Success', response_fields)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(404, 'Domain not found', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def post(self, domain):
|
||||
""" Generate new DKIM/DMARC keys for domain """
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain_found = models.Domain.query.get(domain)
|
||||
if not domain_found:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
domain_found.generate_dkim_key()
|
||||
domain_found.save_dkim_key()
|
||||
return {'code': 200, 'message': f'DKIM/DMARC keys have been generated for domain {domain}'}, 200
|
||||
|
||||
@dom.route('/<domain>/manager')
|
||||
class Manager(Resource):
|
||||
@dom.doc('list_managers')
|
||||
@dom.marshal_with(manager_fields, as_list=True, skip_none=True, mask=None)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(404, 'domain not found', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, domain):
|
||||
""" List managers of domain """
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
if not domain:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
domain = models.Domain.query.filter_by(name=domain)
|
||||
return domain.managers
|
||||
|
||||
@dom.doc('create_manager')
|
||||
@dom.expect(manager_fields_create)
|
||||
@dom.response(200, 'Success', response_fields)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(404, 'User or domain not found', response_fields)
|
||||
@dom.response(409, 'Duplicate domain manager', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def post(self, domain):
|
||||
""" Create a new domain manager """
|
||||
data = api.payload
|
||||
if not validators.email(data['user_email']):
|
||||
return {'code': 400, 'message': f'Invalid email address {data["user_email"]}'}, 400
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain = models.Domain.query.get(domain)
|
||||
if not domain:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
user = models.User.query.get(data['user_email'])
|
||||
if not user:
|
||||
return { 'code': 404, 'message': f'User {data["user_email"]} does not exist'}, 404
|
||||
if user in domain.managers:
|
||||
return {'code': 409, 'message': f'User {data["user_email"]} is already a manager of the domain {domain} '}, 409
|
||||
domain.managers.append(user)
|
||||
models.db.session.commit()
|
||||
return {'code': 200, 'message': f'User {data["user_email"]} has been added as manager of the domain {domain} '},200
|
||||
|
||||
@dom.route('/<domain>/manager/<email>')
|
||||
class Domain(Resource):
|
||||
@dom.doc('find_manager')
|
||||
@dom.response(200, 'Success', manager_fields)
|
||||
@dom.response(404, 'Manager not found', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, domain, email):
|
||||
""" Find manager by email address """
|
||||
if not validators.email(email):
|
||||
return {'code': 400, 'message': f'Invalid email address {email}'}, 400
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain = models.Domain.query.get(domain)
|
||||
if not domain:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
user = models.User.query.get(email)
|
||||
if not user:
|
||||
return { 'code': 404, 'message': f'User {email} does not exist'}, 404
|
||||
if user in domain.managers:
|
||||
for manager in domain.managers:
|
||||
if manager.email == email:
|
||||
return marshal(manager, manager_fields),200
|
||||
else:
|
||||
return { 'code': 404, 'message': f'User {email} is not a manager of the domain {domain}'}, 404
|
||||
|
||||
|
||||
@dom.doc('delete_manager')
|
||||
@dom.response(200, 'Success', response_fields)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(404, 'Manager not found', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def delete(self, domain, email):
|
||||
if not validators.email(email):
|
||||
return {'code': 400, 'message': f'Invalid email address {email}'}, 400
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain = models.Domain.query.get(domain)
|
||||
if not domain:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
user = models.User.query.get(email)
|
||||
if not user:
|
||||
return { 'code': 404, 'message': f'User {email} does not exist'}, 404
|
||||
if user in domain.managers:
|
||||
domain.managers.remove(user)
|
||||
models.db.session.commit()
|
||||
return {'code': 200, 'message': f'User {email} has been removed as a manager of the domain {domain} '},200
|
||||
else:
|
||||
return { 'code': 404, 'message': f'User {email} is not a manager of the domain {domain}'}, 404
|
||||
|
||||
@dom.route('/<domain>/users')
|
||||
class User(Resource):
|
||||
@dom.doc('list_user_domain')
|
||||
@dom.marshal_with(user.user_fields_get, as_list=True, skip_none=True, mask=None)
|
||||
@dom.response(400, 'Input validation exception', response_fields)
|
||||
@dom.response(404, 'Domain not found', response_fields)
|
||||
@dom.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, domain):
|
||||
""" List users from domain """
|
||||
if not validators.domain(domain):
|
||||
return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
|
||||
domain_found = models.Domain.query.get(domain)
|
||||
if not domain_found:
|
||||
return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
|
||||
return models.User.query.filter_by(domain=domain_found).all()
|
||||
|
||||
@alt.route('')
|
||||
class Alternatives(Resource):
|
||||
|
||||
@alt.doc('list_alternative')
|
||||
@alt.marshal_with(alternative_fields, as_list=True, skip_none=True, mask=None)
|
||||
@alt.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self):
|
||||
""" List alternatives """
|
||||
return models.Alternative.query.all()
|
||||
|
||||
|
||||
@alt.doc('create_alternative')
|
||||
@alt.expect(alternative_fields)
|
||||
@alt.response(200, 'Success', response_fields)
|
||||
@alt.response(400, 'Input validation exception', response_fields)
|
||||
@alt.response(404, 'Domain not found or missing', response_fields)
|
||||
@alt.response(409, 'Duplicate alternative domain name', response_fields)
|
||||
@alt.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def post(self):
|
||||
""" Create new alternative (for domain) """
|
||||
data = api.payload
|
||||
if not validators.domain(data['name']):
|
||||
return { 'code': 400, 'message': f'Alternative domain {data["name"]} is not a valid domain'}, 400
|
||||
if not validators.domain(data['domain']):
|
||||
return { 'code': 400, 'message': f'Domain {data["domain"]} is not a valid domain'}, 400
|
||||
domain = models.Domain.query.get(data['domain'])
|
||||
if not domain:
|
||||
return { 'code': 404, 'message': f'Domain {data["domain"]} does not exist'}, 404
|
||||
if common.fqdn_in_use(data['name']):
|
||||
return { 'code': 409, 'message': f'Duplicate alternative domain name {data["name"]}'}, 409
|
||||
|
||||
alternative = models.Alternative(name=data['name'], domain_name=data['domain'])
|
||||
models.db.session.add(alternative)
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Alternative {data["name"]} for domain {data["domain"]} has been created'}, 200
|
||||
|
||||
@alt.route('/<string:alt>')
|
||||
class Alternative(Resource):
|
||||
@alt.doc('find_alternative')
|
||||
@alt.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, alt):
|
||||
""" Find alternative (of domain) """
|
||||
if not validators.domain(alt):
|
||||
return { 'code': 400, 'message': f'Alternative domain {alt} is not a valid domain'}, 400
|
||||
alternative = models.Alternative.query.filter_by(name=alt).first()
|
||||
if not alternative:
|
||||
return{ 'code': 404, 'message': f'Alternative domain {alt} does not exist'}, 404
|
||||
return marshal(alternative, alternative_fields), 200
|
||||
|
||||
@alt.doc('delete_alternative')
|
||||
@alt.response(200, 'Success', response_fields)
|
||||
@alt.response(400, 'Input validation exception', response_fields)
|
||||
@alt.response(404, 'Alternative/Domain not found or missing', response_fields)
|
||||
@alt.response(409, 'Duplicate domain name', response_fields)
|
||||
@alt.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def delete(self, alt):
|
||||
""" Delete alternative (for domain) """
|
||||
if not validators.domain(alt):
|
||||
return { 'code': 400, 'message': f'Alternative domain {alt} is not a valid domain'}, 400
|
||||
alternative = models.Alternative.query.filter_by(name=alt).scalar()
|
||||
if not alternative:
|
||||
return { 'code': 404, 'message': f'Alternative domain {alt} does not exist'}, 404
|
||||
domain = alternative.domain_name
|
||||
db.session.delete(alternative)
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Alternative {alt} for domain {domain} has been deleted'}, 200
|
@ -0,0 +1,118 @@
|
||||
from flask_restx import Resource, fields, marshal
|
||||
import validators
|
||||
|
||||
from . import api, response_fields
|
||||
from .. import common
|
||||
from ... import models
|
||||
|
||||
db = models.db
|
||||
|
||||
relay = api.namespace('relay', description='Relay operations')
|
||||
|
||||
relay_fields = api.model('Relay', {
|
||||
'name': fields.String(description='relayed domain name', example='example.com', required=True),
|
||||
'smtp': fields.String(description='remote host', example='example.com', required=False),
|
||||
'comment': fields.String(description='a comment', required=False)
|
||||
})
|
||||
|
||||
relay_fields_update = api.model('RelayUpdate', {
|
||||
'smtp': fields.String(description='remote host', example='example.com', required=False),
|
||||
'comment': fields.String(description='a comment', required=False)
|
||||
})
|
||||
|
||||
@relay.route('')
|
||||
class Relays(Resource):
|
||||
@relay.doc('list_relays')
|
||||
@relay.marshal_with(relay_fields, as_list=True, skip_none=True, mask=None)
|
||||
@relay.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self):
|
||||
"List relays"
|
||||
return models.Relay.query.all()
|
||||
|
||||
@relay.doc('create_relay')
|
||||
@relay.expect(relay_fields)
|
||||
@relay.response(200, 'Success', response_fields)
|
||||
@relay.response(400, 'Input validation exception')
|
||||
@relay.response(409, 'Duplicate relay', response_fields)
|
||||
@relay.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def post(self):
|
||||
""" Create relay """
|
||||
data = api.payload
|
||||
|
||||
if not validators.domain(name):
|
||||
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
|
||||
|
||||
if common.fqdn_in_use(data['name']):
|
||||
return { 'code': 409, 'message': f'Duplicate domain {data["name"]}'}, 409
|
||||
relay_model = models.Relay(name=data['name'])
|
||||
if 'smtp' in data:
|
||||
relay_model.smtp = data['smtp']
|
||||
if 'comment' in data:
|
||||
relay_model.comment = data['comment']
|
||||
db.session.add(relay_model)
|
||||
db.session.commit()
|
||||
return {'code': 200, 'message': f'Relayed domain {data["name"]} has been created'}, 200
|
||||
|
||||
@relay.route('/<string:name>')
|
||||
class Relay(Resource):
|
||||
@relay.doc('find_relay')
|
||||
@relay.response(400, 'Input validation exception', response_fields)
|
||||
@relay.response(404, 'Relay not found', response_fields)
|
||||
@relay.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def get(self, name):
|
||||
""" Find relay """
|
||||
if not validators.domain(name):
|
||||
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
|
||||
|
||||
relay_found = models.Relay.query.filter_by(name=name).first()
|
||||
if relay_found is None:
|
||||
return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
|
||||
return marshal(relay_found, relay_fields), 200
|
||||
|
||||
@relay.doc('update_relay')
|
||||
@relay.expect(relay_fields_update)
|
||||
@relay.response(200, 'Success', response_fields)
|
||||
@relay.response(400, 'Input validation exception', response_fields)
|
||||
@relay.response(404, 'Relay not found', response_fields)
|
||||
@relay.response(409, 'Duplicate relay', response_fields)
|
||||
@relay.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def patch(self, name):
|
||||
""" Update relay """
|
||||
data = api.payload
|
||||
|
||||
if not validators.domain(name):
|
||||
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
|
||||
|
||||
relay_found = models.Relay.query.filter_by(name=name).first()
|
||||
if relay_found is None:
|
||||
return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
|
||||
|
||||
if 'smtp' in data:
|
||||
relay_found.smtp = data['smtp']
|
||||
if 'comment' in data:
|
||||
relay_found.comment = data['comment']
|
||||
db.session.add(relay_found)
|
||||
db.session.commit()
|
||||
return { 'code': 200, 'message': f'Relayed domain {name} has been updated'}, 200
|
||||
|
||||
|
||||
@relay.doc('delete_relay')
|
||||
@relay.response(200, 'Success', response_fields)
|
||||
@relay.response(400, 'Input validation exception', response_fields)
|
||||
@relay.response(404, 'Relay not found', response_fields)
|
||||
@relay.doc(security='Bearer')
|
||||
@common.api_token_authorization
|
||||
def delete(self, name):
|
||||
""" Delete relay """
|
||||
if not validators.domain(name):
|
||||
return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
|
||||
relay_found = models.Relay.query.filter_by(name=name).first()
|
||||
if relay_found is None:
|
||||
return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
|
||||
db.session.delete(relay_found)
|
||||
db.session.commit()
|
||||
return { 'code': 200, 'message': f'Relayed domain {name} has been deleted'}, 200
|
@ -0,0 +1,733 @@
|
||||
# Czech translations for Mailu.io.
|
||||
# Copyright (C) 2023 S474N
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# S474N <translate@s474n.com>, 2023.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: translate@s474n.com\n"
|
||||
"POT-Creation-Date: 2022-05-22 18:47+0200\n"
|
||||
"PO-Revision-Date: 2023-02-21 16:14+0100\n"
|
||||
"Last-Translator: S474N <translate@s474n.com>\n"
|
||||
"Language-Team: Czech\n"
|
||||
"Language: cs_CZ\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
"X-Generator: Poedit 3.2.2\n"
|
||||
|
||||
#: mailu/sso/forms.py:8 mailu/ui/forms.py:79
|
||||
msgid "E-mail"
|
||||
msgstr "E-mail"
|
||||
|
||||
#: mailu/sso/forms.py:9 mailu/ui/forms.py:80 mailu/ui/forms.py:93
|
||||
#: mailu/ui/forms.py:112 mailu/ui/forms.py:166
|
||||
#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:57
|
||||
msgid "Password"
|
||||
msgstr "Heslo"
|
||||
|
||||
#: mailu/sso/forms.py:10 mailu/sso/forms.py:11 mailu/sso/templates/login.html:4
|
||||
#: mailu/ui/templates/sidebar.html:142
|
||||
msgid "Sign in"
|
||||
msgstr "Přihlásit se"
|
||||
|
||||
#: mailu/sso/templates/base_sso.html:8 mailu/ui/templates/base.html:8
|
||||
msgid "Admin page for"
|
||||
msgstr "Admin stránka pro"
|
||||
|
||||
#: mailu/sso/templates/base_sso.html:19 mailu/ui/templates/base.html:19
|
||||
msgid "toggle sidebar"
|
||||
msgstr "přepnout postranní panel"
|
||||
|
||||
#: mailu/sso/templates/base_sso.html:37 mailu/ui/templates/base.html:37
|
||||
msgid "change language"
|
||||
msgstr "změnit jazyk"
|
||||
|
||||
#: mailu/sso/templates/sidebar_sso.html:4 mailu/ui/templates/sidebar.html:94
|
||||
msgid "Go to"
|
||||
msgstr "Jít"
|
||||
|
||||
#: mailu/sso/templates/sidebar_sso.html:9 mailu/ui/templates/client.html:4
|
||||
#: mailu/ui/templates/sidebar.html:50 mailu/ui/templates/sidebar.html:107
|
||||
msgid "Client setup"
|
||||
msgstr "Nastavení klienta"
|
||||
|
||||
#: mailu/sso/templates/sidebar_sso.html:16 mailu/ui/templates/sidebar.html:114
|
||||
msgid "Website"
|
||||
msgstr "Webová stránka"
|
||||
|
||||
#: mailu/sso/templates/sidebar_sso.html:22 mailu/ui/templates/sidebar.html:120
|
||||
msgid "Help"
|
||||
msgstr "Pomoc"
|
||||
|
||||
#: mailu/sso/templates/sidebar_sso.html:35
|
||||
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:127
|
||||
msgid "Register a domain"
|
||||
msgstr "Registrovat doménu"
|
||||
|
||||
#: mailu/sso/templates/sidebar_sso.html:49 mailu/ui/forms.py:95
|
||||
#: mailu/ui/templates/sidebar.html:149 mailu/ui/templates/user/signup.html:4
|
||||
#: mailu/ui/templates/user/signup_domain.html:4
|
||||
msgid "Sign up"
|
||||
msgstr "Registrovat se"
|
||||
|
||||
#: mailu/ui/forms.py:33 mailu/ui/forms.py:36
|
||||
msgid "Invalid email address."
|
||||
msgstr "Špatná mailová adresa."
|
||||
|
||||
#: mailu/ui/forms.py:45
|
||||
msgid "Confirm"
|
||||
msgstr "Potvrdit"
|
||||
|
||||
#: mailu/ui/forms.py:48 mailu/ui/forms.py:58
|
||||
#: mailu/ui/templates/domain/details.html:26
|
||||
#: mailu/ui/templates/domain/list.html:19 mailu/ui/templates/relay/list.html:18
|
||||
msgid "Domain name"
|
||||
msgstr "Název domény"
|
||||
|
||||
#: mailu/ui/forms.py:49
|
||||
msgid "Maximum user count"
|
||||
msgstr "Maximální počet uživatelů"
|
||||
|
||||
#: mailu/ui/forms.py:50
|
||||
msgid "Maximum alias count"
|
||||
msgstr "Maximální počet aliasů"
|
||||
|
||||
#: mailu/ui/forms.py:51
|
||||
msgid "Maximum user quota"
|
||||
msgstr "Maximální uživatelská kvóta"
|
||||
|
||||
#: mailu/ui/forms.py:52
|
||||
msgid "Enable sign-up"
|
||||
msgstr "Povolit registraci"
|
||||
|
||||
#: mailu/ui/forms.py:53 mailu/ui/forms.py:74 mailu/ui/forms.py:86
|
||||
#: mailu/ui/forms.py:132 mailu/ui/forms.py:144
|
||||
#: mailu/ui/templates/alias/list.html:22 mailu/ui/templates/domain/list.html:22
|
||||
#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:20
|
||||
#: mailu/ui/templates/user/list.html:24
|
||||
msgid "Comment"
|
||||
msgstr "Komentář"
|
||||
|
||||
#: mailu/ui/forms.py:54 mailu/ui/forms.py:68 mailu/ui/forms.py:75
|
||||
#: mailu/ui/forms.py:88 mailu/ui/forms.py:136 mailu/ui/forms.py:145
|
||||
msgid "Save"
|
||||
msgstr "Uložit"
|
||||
|
||||
#: mailu/ui/forms.py:59
|
||||
msgid "Initial admin"
|
||||
msgstr "Hlavní admin"
|
||||
|
||||
#: mailu/ui/forms.py:60
|
||||
msgid "Admin password"
|
||||
msgstr "Heslo admina"
|
||||
|
||||
#: mailu/ui/forms.py:61 mailu/ui/forms.py:81 mailu/ui/forms.py:94
|
||||
msgid "Confirm password"
|
||||
msgstr "Potvrdit heslo"
|
||||
|
||||
#: mailu/ui/forms.py:63
|
||||
msgid "Create"
|
||||
msgstr "Vytvořit"
|
||||
|
||||
#: mailu/ui/forms.py:67
|
||||
msgid "Alternative name"
|
||||
msgstr "Alternativní jméno"
|
||||
|
||||
#: mailu/ui/forms.py:72
|
||||
msgid "Relayed domain name"
|
||||
msgstr "Seznam předávaných domén"
|
||||
|
||||
#: mailu/ui/forms.py:73 mailu/ui/templates/relay/list.html:19
|
||||
msgid "Remote host"
|
||||
msgstr "Vzdálený hostitel"
|
||||
|
||||
#: mailu/ui/forms.py:82 mailu/ui/templates/user/list.html:23
|
||||
#: mailu/ui/templates/user/signup_domain.html:16
|
||||
msgid "Quota"
|
||||
msgstr "Kvóta"
|
||||
|
||||
#: mailu/ui/forms.py:83
|
||||
msgid "Allow IMAP access"
|
||||
msgstr "Povolit přístup IMAP"
|
||||
|
||||
#: mailu/ui/forms.py:84
|
||||
msgid "Allow POP3 access"
|
||||
msgstr "Povolit přístup POP3"
|
||||
|
||||
#: mailu/ui/forms.py:85 mailu/ui/forms.py:101
|
||||
#: mailu/ui/templates/user/settings.html:15
|
||||
msgid "Displayed name"
|
||||
msgstr "Zobrazené jméno"
|
||||
|
||||
#: mailu/ui/forms.py:87
|
||||
msgid "Enabled"
|
||||
msgstr "Povoleno"
|
||||
|
||||
#: mailu/ui/forms.py:92
|
||||
msgid "Email address"
|
||||
msgstr "Emailová adresa"
|
||||
|
||||
#: mailu/ui/forms.py:102
|
||||
msgid "Enable spam filter"
|
||||
msgstr "Povolit filtr spamu"
|
||||
|
||||
#: mailu/ui/forms.py:103
|
||||
msgid "Enable marking spam mails as read"
|
||||
msgstr "Povolit označování spamových e-mailů jako přečtených"
|
||||
|
||||
#: mailu/ui/forms.py:104
|
||||
msgid "Spam filter tolerance"
|
||||
msgstr "Tolerance spamového filtru"
|
||||
|
||||
#: mailu/ui/forms.py:105
|
||||
msgid "Enable forwarding"
|
||||
msgstr "Povolit přeposílání"
|
||||
|
||||
#: mailu/ui/forms.py:106
|
||||
msgid "Keep a copy of the emails"
|
||||
msgstr "Zachovat kopii e-mailů"
|
||||
|
||||
#: mailu/ui/forms.py:107 mailu/ui/forms.py:143
|
||||
#: mailu/ui/templates/alias/list.html:21
|
||||
msgid "Destination"
|
||||
msgstr "Cíl"
|
||||
|
||||
#: mailu/ui/forms.py:108
|
||||
msgid "Save settings"
|
||||
msgstr "Uložit nastavení"
|
||||
|
||||
#: mailu/ui/forms.py:113
|
||||
msgid "Password check"
|
||||
msgstr "Kontrola hesla"
|
||||
|
||||
#: mailu/ui/forms.py:114 mailu/ui/templates/sidebar.html:25
|
||||
msgid "Update password"
|
||||
msgstr "Aktualizovat heslo"
|
||||
|
||||
#: mailu/ui/forms.py:118
|
||||
msgid "Enable automatic reply"
|
||||
msgstr "Povolit automatickou odpověď"
|
||||
|
||||
#: mailu/ui/forms.py:119
|
||||
msgid "Reply subject"
|
||||
msgstr "Předmět odpovědi"
|
||||
|
||||
#: mailu/ui/forms.py:120
|
||||
msgid "Reply body"
|
||||
msgstr "Tělo odpovědi"
|
||||
|
||||
#: mailu/ui/forms.py:122
|
||||
msgid "Start of vacation"
|
||||
msgstr "Začátek dovolené"
|
||||
|
||||
#: mailu/ui/forms.py:123
|
||||
msgid "End of vacation"
|
||||
msgstr "Konec dovolené"
|
||||
|
||||
#: mailu/ui/forms.py:124
|
||||
msgid "Update"
|
||||
msgstr "Aktualizovat"
|
||||
|
||||
#: mailu/ui/forms.py:129
|
||||
msgid "Your token (write it down, as it will never be displayed again)"
|
||||
msgstr "Váš token (zapište si ho, protože se již nikdy nezobrazí)"
|
||||
|
||||
#: mailu/ui/forms.py:134 mailu/ui/templates/token/list.html:21
|
||||
msgid "Authorized IP"
|
||||
msgstr "Autorizovaná IP"
|
||||
|
||||
#: mailu/ui/forms.py:140
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: mailu/ui/forms.py:142
|
||||
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
|
||||
msgstr "Použít syntaxi jako SQL (např. pro doménové koše)"
|
||||
|
||||
#: mailu/ui/forms.py:149
|
||||
msgid "Admin email"
|
||||
msgstr "Email admina"
|
||||
|
||||
#: mailu/ui/forms.py:150 mailu/ui/forms.py:155 mailu/ui/forms.py:168
|
||||
msgid "Submit"
|
||||
msgstr "Poslat"
|
||||
|
||||
#: mailu/ui/forms.py:154
|
||||
msgid "Manager email"
|
||||
msgstr "E-mail manažera"
|
||||
|
||||
#: mailu/ui/forms.py:159
|
||||
msgid "Protocol"
|
||||
msgstr "Protokol"
|
||||
|
||||
#: mailu/ui/forms.py:162
|
||||
msgid "Hostname or IP"
|
||||
msgstr "Hostitel nebo IP"
|
||||
|
||||
#: mailu/ui/forms.py:163 mailu/ui/templates/client.html:20
|
||||
#: mailu/ui/templates/client.html:45
|
||||
msgid "TCP port"
|
||||
msgstr "TCP port"
|
||||
|
||||
#: mailu/ui/forms.py:164
|
||||
msgid "Enable TLS"
|
||||
msgstr "Povolit TLS"
|
||||
|
||||
#: mailu/ui/forms.py:165 mailu/ui/templates/client.html:28
|
||||
#: mailu/ui/templates/client.html:53 mailu/ui/templates/fetch/list.html:21
|
||||
msgid "Username"
|
||||
msgstr "Uživatelské jméno"
|
||||
|
||||
#: mailu/ui/forms.py:167
|
||||
msgid "Keep emails on the server"
|
||||
msgstr "Zachovat e-maily na serveru"
|
||||
|
||||
#: mailu/ui/forms.py:172
|
||||
msgid "Announcement subject"
|
||||
msgstr "Předmět oznámení"
|
||||
|
||||
#: mailu/ui/forms.py:174
|
||||
msgid "Announcement body"
|
||||
msgstr "Tělo oznámení"
|
||||
|
||||
#: mailu/ui/forms.py:176
|
||||
msgid "Send"
|
||||
msgstr "Poslat"
|
||||
|
||||
#: mailu/ui/templates/announcement.html:4
|
||||
msgid "Public announcement"
|
||||
msgstr "Veřejné oznámení"
|
||||
|
||||
#: mailu/ui/templates/antispam.html:4 mailu/ui/templates/sidebar.html:80
|
||||
#: mailu/ui/templates/user/settings.html:19
|
||||
msgid "Antispam"
|
||||
msgstr "Antispam"
|
||||
|
||||
#: mailu/ui/templates/antispam.html:8
|
||||
msgid "RSPAMD status page"
|
||||
msgstr "Stavová stránka RSPAMD"
|
||||
|
||||
#: mailu/ui/templates/client.html:8
|
||||
msgid "configure your email client"
|
||||
msgstr "nakonfigurovat e-mailového klienta"
|
||||
|
||||
#: mailu/ui/templates/client.html:13
|
||||
msgid "Incoming mail"
|
||||
msgstr "Příchozí mail"
|
||||
|
||||
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:41
|
||||
msgid "Mail protocol"
|
||||
msgstr "Poštovní protokol"
|
||||
|
||||
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:49
|
||||
msgid "Server name"
|
||||
msgstr "Název serveru"
|
||||
|
||||
#: mailu/ui/templates/client.html:38
|
||||
msgid "Outgoing mail"
|
||||
msgstr "Odchozí pošta"
|
||||
|
||||
#: mailu/ui/templates/confirm.html:4
|
||||
msgid "Confirm action"
|
||||
msgstr "Potvrdit akci"
|
||||
|
||||
#: mailu/ui/templates/confirm.html:13
|
||||
#, python-format
|
||||
msgid "You are about to %(action)s. Please confirm your action."
|
||||
msgstr "Chystáte se %(action)s. Potvrďte prosím vaši akci."
|
||||
|
||||
#: mailu/ui/templates/docker-error.html:4
|
||||
msgid "Docker error"
|
||||
msgstr "Chyba Dockeru"
|
||||
|
||||
#: mailu/ui/templates/docker-error.html:12
|
||||
msgid "An error occurred while talking to the Docker server."
|
||||
msgstr "Při komunikaci se serverem Docker došlo k chybě."
|
||||
|
||||
#: mailu/ui/templates/macros.html:129
|
||||
msgid "copy to clipboard"
|
||||
msgstr "zkopírovat do schránky"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:15
|
||||
msgid "My account"
|
||||
msgstr "Můj účet"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:19 mailu/ui/templates/user/list.html:37
|
||||
msgid "Settings"
|
||||
msgstr "Nastavení"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/user/list.html:38
|
||||
msgid "Auto-reply"
|
||||
msgstr "Automatická odpověď"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:37
|
||||
#: mailu/ui/templates/user/list.html:39
|
||||
msgid "Fetched accounts"
|
||||
msgstr "Fetched účty"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:43 mailu/ui/templates/token/list.html:4
|
||||
msgid "Authentication tokens"
|
||||
msgstr "Autentizační tokeny"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:56
|
||||
msgid "Administration"
|
||||
msgstr "Administrace"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:62
|
||||
msgid "Announcement"
|
||||
msgstr "Oznámení"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:68
|
||||
msgid "Administrators"
|
||||
msgstr "Administrátoři"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:74
|
||||
msgid "Relayed domains"
|
||||
msgstr "Relayované domény"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:88
|
||||
msgid "Mail domains"
|
||||
msgstr "Poštovní domény"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:99
|
||||
msgid "Webmail"
|
||||
msgstr "Webmail"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:135
|
||||
msgid "Sign out"
|
||||
msgstr "Odhlásit se"
|
||||
|
||||
#: mailu/ui/templates/working.html:4
|
||||
msgid "We are still working on this feature!"
|
||||
msgstr "Na této funkci stále pracujeme!"
|
||||
|
||||
#: mailu/ui/templates/admin/create.html:4
|
||||
msgid "Add a global administrator"
|
||||
msgstr "Přidat globálního administrátora"
|
||||
|
||||
#: mailu/ui/templates/admin/list.html:4
|
||||
msgid "Global administrators"
|
||||
msgstr "Globální administrátor"
|
||||
|
||||
#: mailu/ui/templates/admin/list.html:9
|
||||
msgid "Add administrator"
|
||||
msgstr "Přidat administrátora"
|
||||
|
||||
#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19
|
||||
#: mailu/ui/templates/alternative/list.html:19
|
||||
#: mailu/ui/templates/domain/list.html:17 mailu/ui/templates/fetch/list.html:19
|
||||
#: mailu/ui/templates/manager/list.html:19
|
||||
#: mailu/ui/templates/relay/list.html:17 mailu/ui/templates/token/list.html:19
|
||||
#: mailu/ui/templates/user/list.html:19
|
||||
msgid "Actions"
|
||||
msgstr "Akce"
|
||||
|
||||
#: mailu/ui/templates/admin/list.html:18 mailu/ui/templates/alias/list.html:20
|
||||
#: mailu/ui/templates/manager/list.html:20 mailu/ui/templates/user/list.html:21
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
#: mailu/ui/templates/admin/list.html:25 mailu/ui/templates/alias/list.html:32
|
||||
#: mailu/ui/templates/alternative/list.html:29
|
||||
#: mailu/ui/templates/domain/list.html:34 mailu/ui/templates/fetch/list.html:34
|
||||
#: mailu/ui/templates/manager/list.html:27
|
||||
#: mailu/ui/templates/relay/list.html:30 mailu/ui/templates/token/list.html:30
|
||||
#: mailu/ui/templates/user/list.html:34
|
||||
msgid "Delete"
|
||||
msgstr "Vymazat"
|
||||
|
||||
#: mailu/ui/templates/alias/create.html:4
|
||||
msgid "Create alias"
|
||||
msgstr "Vytvořit alias"
|
||||
|
||||
#: mailu/ui/templates/alias/edit.html:4
|
||||
msgid "Edit alias"
|
||||
msgstr "Upravit alias"
|
||||
|
||||
#: mailu/ui/templates/alias/list.html:4
|
||||
msgid "Alias list"
|
||||
msgstr "Seznam aliasů"
|
||||
|
||||
#: mailu/ui/templates/alias/list.html:12
|
||||
msgid "Add alias"
|
||||
msgstr "Přidat alias"
|
||||
|
||||
#: mailu/ui/templates/alias/list.html:23
|
||||
#: mailu/ui/templates/alternative/list.html:21
|
||||
#: mailu/ui/templates/domain/list.html:23 mailu/ui/templates/fetch/list.html:25
|
||||
#: mailu/ui/templates/relay/list.html:21 mailu/ui/templates/token/list.html:22
|
||||
#: mailu/ui/templates/user/list.html:25
|
||||
msgid "Created"
|
||||
msgstr "Vytvořeno"
|
||||
|
||||
#: mailu/ui/templates/alias/list.html:24
|
||||
#: mailu/ui/templates/alternative/list.html:22
|
||||
#: mailu/ui/templates/domain/list.html:24 mailu/ui/templates/fetch/list.html:26
|
||||
#: mailu/ui/templates/relay/list.html:22 mailu/ui/templates/token/list.html:23
|
||||
#: mailu/ui/templates/user/list.html:26
|
||||
msgid "Last edit"
|
||||
msgstr "Poslední úprava"
|
||||
|
||||
#: mailu/ui/templates/alias/list.html:31 mailu/ui/templates/domain/list.html:33
|
||||
#: mailu/ui/templates/fetch/list.html:33 mailu/ui/templates/relay/list.html:29
|
||||
#: mailu/ui/templates/user/list.html:33
|
||||
msgid "Edit"
|
||||
msgstr "Upravit"
|
||||
|
||||
#: mailu/ui/templates/alternative/create.html:4
|
||||
msgid "Create alternative domain"
|
||||
msgstr "Vytvořit alternativní doménu"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:4
|
||||
msgid "Alternative domain list"
|
||||
msgstr "Seznam alternativních domén"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:12
|
||||
msgid "Add alternative"
|
||||
msgstr "Přidat alternativu"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:20
|
||||
msgid "Name"
|
||||
msgstr "Jméno"
|
||||
|
||||
#: mailu/ui/templates/domain/create.html:4
|
||||
#: mailu/ui/templates/domain/list.html:9
|
||||
msgid "New domain"
|
||||
msgstr "Nová doména"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:4
|
||||
msgid "Domain details"
|
||||
msgstr "Podrobnosti o doméně"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:15
|
||||
msgid "Regenerate keys"
|
||||
msgstr "Obnovit klíče"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:17
|
||||
msgid "Generate keys"
|
||||
msgstr "Generovat klíče"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:30
|
||||
msgid "DNS MX entry"
|
||||
msgstr "Záznam DNS MX"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:34
|
||||
msgid "DNS SPF entries"
|
||||
msgstr "Záznamy DNS SPF"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:40
|
||||
msgid "DKIM public key"
|
||||
msgstr "Veřejný klíč DKIM"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:44
|
||||
msgid "DNS DKIM entry"
|
||||
msgstr "Záznam DNS DKIM"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:48
|
||||
msgid "DNS DMARC entry"
|
||||
msgstr "Záznam DNS DMARC"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:58
|
||||
msgid "DNS TLSA entry"
|
||||
msgstr "Záznam DNS TLSA"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:63
|
||||
msgid "DNS client auto-configuration entries"
|
||||
msgstr "Položky automatické konfigurace klienta DNS"
|
||||
|
||||
#: mailu/ui/templates/domain/edit.html:4
|
||||
msgid "Edit domain"
|
||||
msgstr "Upravit doménu"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:4
|
||||
msgid "Domain list"
|
||||
msgstr "Seznam domén"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:18
|
||||
msgid "Manage"
|
||||
msgstr "Spravovat"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:20
|
||||
msgid "Mailbox count"
|
||||
msgstr "Počet poštovních schránek"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:21
|
||||
msgid "Alias count"
|
||||
msgstr "Počet aliasů"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:31
|
||||
msgid "Details"
|
||||
msgstr "Podrobnosti"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:38
|
||||
msgid "Users"
|
||||
msgstr "Uživatelů"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:39
|
||||
msgid "Aliases"
|
||||
msgstr "Aliasů"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:40
|
||||
msgid "Managers"
|
||||
msgstr "Manažerů"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:42
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternativ"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:13
|
||||
msgid ""
|
||||
"In order to register a new domain, you must first setup the\n"
|
||||
" domain zone so that the domain <code>MX</code> points to this server"
|
||||
msgstr ""
|
||||
"Chcete-li zaregistrovat novou doménu, musíte nejprve nastavit\n"
|
||||
" zónu domény tak, aby doménový <code>MX</code> záznam ukazovala na tento "
|
||||
"server"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:18
|
||||
msgid ""
|
||||
"If you do not know how to setup an <code>MX</code> record for your DNS "
|
||||
"zone,\n"
|
||||
" please contact your DNS provider or administrator. Also, please wait a\n"
|
||||
" couple minutes after the <code>MX</code> is set so the local server "
|
||||
"cache\n"
|
||||
" expires."
|
||||
msgstr ""
|
||||
"Pokud nevíte, jak nastavit <code>MX</code> záznam pro zónu DNS,\n"
|
||||
" kontaktujte svého poskytovatele DNS nebo správce. Také prosím počkejte "
|
||||
"a\n"
|
||||
" několik minut po <code>MX</code> tak, aby vypršela v mezipaměti "
|
||||
"místního\n"
|
||||
" serveru."
|
||||
|
||||
#: mailu/ui/templates/fetch/create.html:4
|
||||
msgid "Add a fetched account"
|
||||
msgstr "Přidejte fetched účet"
|
||||
|
||||
#: mailu/ui/templates/fetch/edit.html:4
|
||||
msgid "Update a fetched account"
|
||||
msgstr "Aktualizujte fetched účet"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:12
|
||||
msgid "Add an account"
|
||||
msgstr "Přidat účet"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:20
|
||||
msgid "Endpoint"
|
||||
msgstr "Koncový bod"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:22
|
||||
msgid "Keep emails"
|
||||
msgstr "Zachovat emaily"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:23
|
||||
msgid "Last check"
|
||||
msgstr "Poslední kontrola"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:24
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:38
|
||||
msgid "yes"
|
||||
msgstr "ano"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:38
|
||||
msgid "no"
|
||||
msgstr "ne"
|
||||
|
||||
#: mailu/ui/templates/manager/create.html:4
|
||||
msgid "Add a manager"
|
||||
msgstr "Přidat manažera"
|
||||
|
||||
#: mailu/ui/templates/manager/list.html:4
|
||||
msgid "Manager list"
|
||||
msgstr "Seznam manažerů"
|
||||
|
||||
#: mailu/ui/templates/manager/list.html:12
|
||||
msgid "Add manager"
|
||||
msgstr "Přidat manažera"
|
||||
|
||||
#: mailu/ui/templates/relay/create.html:4
|
||||
msgid "New relay domain"
|
||||
msgstr "Nová relay doména"
|
||||
|
||||
#: mailu/ui/templates/relay/edit.html:4
|
||||
msgid "Edit relayed domain"
|
||||
msgstr "Upravit relay doménu"
|
||||
|
||||
#: mailu/ui/templates/relay/list.html:4
|
||||
msgid "Relayed domain list"
|
||||
msgstr "Seznam relay domén"
|
||||
|
||||
#: mailu/ui/templates/relay/list.html:9
|
||||
msgid "New relayed domain"
|
||||
msgstr "Nová relay doména"
|
||||
|
||||
#: mailu/ui/templates/token/create.html:4
|
||||
msgid "Create an authentication token"
|
||||
msgstr "Vytvořit ověřovací token"
|
||||
|
||||
#: mailu/ui/templates/token/list.html:12
|
||||
msgid "New token"
|
||||
msgstr "Nový token"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:4
|
||||
msgid "New user"
|
||||
msgstr "Nový uživatel"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:15
|
||||
msgid "General"
|
||||
msgstr "Všeobecné"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:23
|
||||
msgid "Features and quotas"
|
||||
msgstr "Funkce a kvóty"
|
||||
|
||||
#: mailu/ui/templates/user/edit.html:4
|
||||
msgid "Edit user"
|
||||
msgstr "Upravit uživatele"
|
||||
|
||||
#: mailu/ui/templates/user/list.html:4
|
||||
msgid "User list"
|
||||
msgstr "Seznam uživatelů"
|
||||
|
||||
#: mailu/ui/templates/user/list.html:12
|
||||
msgid "Add user"
|
||||
msgstr "Přidat uživatele"
|
||||
|
||||
#: mailu/ui/templates/user/list.html:20 mailu/ui/templates/user/settings.html:4
|
||||
msgid "User settings"
|
||||
msgstr "Uživatelské nastavení"
|
||||
|
||||
#: mailu/ui/templates/user/list.html:22
|
||||
msgid "Features"
|
||||
msgstr "Funkce"
|
||||
|
||||
#: mailu/ui/templates/user/password.html:4
|
||||
msgid "Password update"
|
||||
msgstr "Aktualizace hesla"
|
||||
|
||||
#: mailu/ui/templates/user/reply.html:4
|
||||
msgid "Automatic reply"
|
||||
msgstr "Automatická odpověď"
|
||||
|
||||
#: mailu/ui/templates/user/settings.html:27
|
||||
msgid "Auto-forward"
|
||||
msgstr "Automatické přeposlání"
|
||||
|
||||
#: mailu/ui/templates/user/signup_domain.html:8
|
||||
msgid "pick a domain for the new account"
|
||||
msgstr "vybrat doménu pro nový účet"
|
||||
|
||||
#: mailu/ui/templates/user/signup_domain.html:14
|
||||
msgid "Domain"
|
||||
msgstr "Doména"
|
||||
|
||||
#: mailu/ui/templates/user/signup_domain.html:15
|
||||
msgid "Available slots"
|
||||
msgstr "Dostupných slotů"
|
@ -0,0 +1,22 @@
|
||||
""" Add user.allow_spoofing
|
||||
|
||||
Revision ID: 7ac252f2bbbf
|
||||
Revises: 8f9ea78776f4
|
||||
Create Date: 2022-11-20 08:57:16.879152
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7ac252f2bbbf'
|
||||
down_revision = 'f4f0f89e0047'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('user', sa.Column('allow_spoofing', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('user', 'allow_spoofing')
|
@ -0,0 +1,25 @@
|
||||
""" Add fetch.scan and fetch.folders
|
||||
|
||||
Revision ID: f4f0f89e0047
|
||||
Revises: 8f9ea78776f4
|
||||
Create Date: 2022-11-13 16:29:01.246509
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f4f0f89e0047'
|
||||
down_revision = '8f9ea78776f4'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import mailu
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table('fetch') as batch:
|
||||
batch.add_column(sa.Column('scan', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
|
||||
batch.add_column(sa.Column('folders', mailu.models.CommaSeparatedList(), nullable=True))
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('fetch') as batch:
|
||||
batch.drop_column('folders')
|
||||
batch.drop_column('scan')
|
@ -1,78 +0,0 @@
|
||||
alembic==1.7.4
|
||||
appdirs==1.4.4
|
||||
Babel==2.9.1
|
||||
bcrypt==3.2.0
|
||||
blinker==1.4
|
||||
CacheControl==0.12.9
|
||||
certifi==2021.10.8
|
||||
# cffi==1.15.0
|
||||
chardet==4.0.0
|
||||
click==8.0.3
|
||||
colorama==0.4.4
|
||||
contextlib2==21.6.0
|
||||
cryptography==35.0.0
|
||||
decorator==5.1.0
|
||||
# distlib==0.3.1
|
||||
# distro==1.5.0
|
||||
dnspython==2.1.0
|
||||
dominate==2.6.0
|
||||
email-validator==1.1.3
|
||||
Flask==2.0.2
|
||||
Flask-Babel==2.0.0
|
||||
Flask-Bootstrap==3.3.7.1
|
||||
Flask-DebugToolbar==0.11.0
|
||||
Flask-Limiter==1.4
|
||||
Flask-Login==0.5.0
|
||||
flask-marshmallow==0.14.0
|
||||
Flask-Migrate==3.1.0
|
||||
Flask-Script==2.0.6
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-WTF==0.15.1
|
||||
greenlet==1.1.2
|
||||
gunicorn==20.1.0
|
||||
html5lib==1.1
|
||||
idna==3.3
|
||||
infinity==1.5
|
||||
intervals==0.9.2
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.2
|
||||
limits==1.5.1
|
||||
lockfile==0.12.2
|
||||
Mako==1.1.5
|
||||
MarkupSafe==2.0.1
|
||||
marshmallow==3.14.0
|
||||
marshmallow-sqlalchemy==0.26.1
|
||||
msgpack==1.0.2
|
||||
# mysqlclient==2.0.3
|
||||
mysql-connector-python==8.0.25
|
||||
ordered-set==4.0.2
|
||||
# packaging==20.9
|
||||
passlib==1.7.4
|
||||
# pep517==0.10.0
|
||||
progress==1.6
|
||||
#psycopg2==2.9.1
|
||||
psycopg2-binary==2.9.3
|
||||
pycparser==2.20
|
||||
Pygments==2.10.0
|
||||
pyOpenSSL==21.0.0
|
||||
pyparsing==3.0.4
|
||||
pytz==2021.3
|
||||
PyYAML==6.0
|
||||
redis==3.5.3
|
||||
requests==2.26.0
|
||||
retrying==1.3.3
|
||||
# six==1.15.0
|
||||
socrate==0.2.0
|
||||
SQLAlchemy==1.4.26
|
||||
srslib==0.1.4
|
||||
tabulate==0.8.9
|
||||
tenacity==8.0.1
|
||||
toml==0.10.2
|
||||
urllib3==1.26.7
|
||||
validators==0.18.2
|
||||
visitor==0.1.3
|
||||
webencodings==0.5.1
|
||||
Werkzeug==2.0.2
|
||||
WTForms==2.3.3
|
||||
WTForms-Components==0.10.5
|
||||
xmltodict==0.12.0
|
@ -1,28 +0,0 @@
|
||||
Flask
|
||||
Flask-Login
|
||||
Flask-SQLAlchemy
|
||||
Flask-bootstrap
|
||||
Flask-Babel
|
||||
Flask-migrate
|
||||
Flask-script
|
||||
Flask-wtf
|
||||
Flask-debugtoolbar
|
||||
limits
|
||||
redis
|
||||
WTForms-Components
|
||||
socrate
|
||||
passlib
|
||||
gunicorn
|
||||
tabulate
|
||||
PyYAML
|
||||
PyOpenSSL
|
||||
Pygments
|
||||
dnspython
|
||||
tenacity
|
||||
mysql-connector-python
|
||||
idna
|
||||
srslib
|
||||
marshmallow
|
||||
flask-marshmallow
|
||||
marshmallow-sqlalchemy
|
||||
xmltodict
|
@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
### CONFIG
|
||||
|
||||
DEV_NAME="${DEV_NAME:-mailu-dev}"
|
||||
DEV_DB="${DEV_DB:-}"
|
||||
DEV_PROFILER="${DEV_PROFILER:-false}"
|
||||
DEV_LISTEN="${DEV_LISTEN:-127.0.0.1:8080}"
|
||||
[[ "${DEV_LISTEN}" == *:* ]] || DEV_LISTEN="127.0.0.1:${DEV_LISTEN}"
|
||||
DEV_ADMIN="${DEV_ADMIN:-admin@example.com}"
|
||||
DEV_PASSWORD="${DEV_PASSWORD:-letmein}"
|
||||
DEV_ARGS=( "$@" )
|
||||
|
||||
### MAIN
|
||||
|
||||
[[ -n "${DEV_DB}" ]] && {
|
||||
[[ -f "${DEV_DB}" ]] || {
|
||||
echo "Sorry, can't find DEV_DB: '${DEV_DB}'"
|
||||
exit 1
|
||||
}
|
||||
DEV_DB="$(realpath "${DEV_DB}")"
|
||||
}
|
||||
|
||||
docker="$(command -v podman || command -v docker || echo false)"
|
||||
[[ "${docker}" == "false" ]] && {
|
||||
echo "Sorry, you'll need podman or docker to run this."
|
||||
exit 1
|
||||
}
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
[[ -n "${tmp}" && -d "${tmp}" ]] || {
|
||||
echo "Sorry, can't create temporary folder."
|
||||
exit 1
|
||||
}
|
||||
trap "rm -rf '${tmp}'" INT TERM EXIT
|
||||
|
||||
admin="$(realpath "$(pwd)/${0%/*}")"
|
||||
base="${admin}/../base"
|
||||
assets="${admin}/assets"
|
||||
|
||||
cd "${tmp}"
|
||||
|
||||
# base
|
||||
cp "${base}"/requirements-* .
|
||||
cp -r "${base}"/libs .
|
||||
sed -E '/^#/d;s:^FROM system$:FROM system AS base:' "${base}/Dockerfile" >Dockerfile
|
||||
|
||||
# assets
|
||||
cp "${assets}/package.json" .
|
||||
cp -r "${assets}/assets" ./assets
|
||||
awk '/new compress/{f=1}!f{print}/}),/{f=0}' <"${assets}/webpack.config.js" >webpack.config.js
|
||||
sed -E '/^#/d;s:^(FROM [^ ]+$):\1 AS assets:' "${assets}/Dockerfile" >>Dockerfile
|
||||
|
||||
# admin
|
||||
sed -E '/^#/d;/^(COPY|EXPOSE|HEALTHCHECK|VOLUME|CMD) /d; s:^(.* )[^ ]*pybabel[^\\]*(.*):\1true \2:' "${admin}/Dockerfile" >>Dockerfile
|
||||
|
||||
# development
|
||||
cat >>Dockerfile <<EOF
|
||||
COPY --from=assets /work/static/ ./static/
|
||||
|
||||
RUN set -euxo pipefail \
|
||||
; mkdir /data \
|
||||
; ln -s /app/audit.py / \
|
||||
; ln -s /app/start.py /
|
||||
|
||||
ENV \
|
||||
FLASK_DEBUG="true" \
|
||||
MEMORY_SESSIONS="true" \
|
||||
RATELIMIT_STORAGE_URL="memory://" \
|
||||
SESSION_COOKIE_SECURE="false" \
|
||||
\
|
||||
DEBUG="true" \
|
||||
DEBUG_PROFILER="${DEV_PROFILER}" \
|
||||
DEBUG_ASSETS="/app/static" \
|
||||
DEBUG_TB_INTERCEPT_REDIRECTS=False \
|
||||
\
|
||||
ADMIN_ADDRESS="127.0.0.1" \
|
||||
FRONT_ADDRESS="127.0.0.1" \
|
||||
SMTP_ADDRESS="127.0.0.1" \
|
||||
IMAP_ADDRESS="127.0.0.1" \
|
||||
REDIS_ADDRESS="127.0.0.1" \
|
||||
ANTIVIRUS_ADDRESS="127.0.0.1" \
|
||||
ANTISPAM_ADDRESS="127.0.0.1" \
|
||||
WEBMAIL_ADDRESS="127.0.0.1" \
|
||||
WEBDAV_ADDRESS="127.0.0.1"
|
||||
|
||||
CMD ["/bin/bash", "-c", "flask db upgrade &>/dev/null && flask mailu admin '${DEV_ADMIN/@*}' '${DEV_ADMIN#*@}' '${DEV_PASSWORD}' --mode ifmissing >/dev/null; flask --debug run --host=0.0.0.0 --port=8080"]
|
||||
EOF
|
||||
|
||||
# build
|
||||
chmod -R u+rwX,go+rX .
|
||||
echo Running: "${docker/*\/}" build --tag "${DEV_NAME}:latest" "${DEV_ARGS[@]}" .
|
||||
"${docker}" build --tag "${DEV_NAME}:latest" "${DEV_ARGS[@]}" .
|
||||
|
||||
# gather volumes to map into container
|
||||
volumes=()
|
||||
|
||||
[[ -n "${DEV_DB}" ]] && volumes+=( --volume "${DEV_DB}:/data/main.db" )
|
||||
|
||||
for vol in audit.py start.py mailu/ migrations/; do
|
||||
volumes+=( --volume "${admin}/${vol}:/app/${vol}" )
|
||||
done
|
||||
|
||||
for file in "${assets}/assets"/*; do
|
||||
[[ ! -f "${file}" || "${file}" == */vendor.js ]] && continue
|
||||
volumes+=( --volume "${file}:/app/static/${file/*\//}" )
|
||||
done
|
||||
|
||||
# show configuration
|
||||
cat <<EOF
|
||||
|
||||
=============================================================================
|
||||
|
||||
The "${DEV_NAME}" container was built using this configuration:
|
||||
|
||||
DEV_NAME="${DEV_NAME}"
|
||||
DEV_DB="${DEV_DB}"
|
||||
DEV_PROFILER="${DEV_PROFILER}"
|
||||
DEV_LISTEN="${DEV_LISTEN}"
|
||||
DEV_ADMIN="${DEV_ADMIN}"
|
||||
DEV_PASSWORD="${DEV_PASSWORD}"
|
||||
DEV_ARGS=( ${DEV_ARGS[*]} )
|
||||
|
||||
=============================================================================
|
||||
|
||||
You can start the container later using this command:
|
||||
|
||||
${docker/*\/} run --rm -it --name "${DEV_NAME}" --publish ${DEV_LISTEN}:8080$(printf " %q" "${volumes[@]}") "${DEV_NAME}"
|
||||
|
||||
=============================================================================
|
||||
|
||||
Enter the running container using this command:
|
||||
${docker/*\/} exec -it "${DEV_NAME}" /bin/bash
|
||||
|
||||
=============================================================================
|
||||
|
||||
To update requirements-prod.txt you can build (and test) using:
|
||||
${docker/*\/} build --tag "${DEV_NAME}:latest" --build-arg MAILU_DEPS=dev .
|
||||
|
||||
And then fetch the new dependencies with:
|
||||
${docker/*\/} exec "${DEV_NAME}" pip freeze >$(realpath "${base}")/requirements-new.txt
|
||||
|
||||
=============================================================================
|
||||
|
||||
The Mailu UI can be found here: http://${DEV_LISTEN}/sso/login
|
||||
EOF
|
||||
[[ -z "${DEV_DB}" ]] && echo "You can log in with user ${DEV_ADMIN} and password ${DEV_PASSWORD}"
|
||||
cat <<EOF
|
||||
|
||||
=============================================================================
|
||||
|
||||
Starting mailu dev environment...
|
||||
EOF
|
||||
|
||||
# run
|
||||
"${docker}" run --rm -it --name "${DEV_NAME}" --publish "${DEV_LISTEN}:8080" "${volumes[@]}" "${DEV_NAME}"
|
||||
|
@ -0,0 +1,91 @@
|
||||
# syntax=docker/dockerfile-upstream:1.4.3
|
||||
|
||||
# base system image (intermediate)
|
||||
ARG DISTRO=alpine:3.17.2
|
||||
FROM $DISTRO as system
|
||||
|
||||
ENV TZ=Etc/UTC LANG=C.UTF-8
|
||||
|
||||
ARG MAILU_UID=1000
|
||||
ARG MAILU_GID=1000
|
||||
|
||||
RUN set -euxo pipefail \
|
||||
; addgroup -Sg ${MAILU_GID} mailu \
|
||||
; adduser -Sg ${MAILU_UID} -G mailu -h /app -g "mailu app" -s /bin/bash mailu \
|
||||
; apk add --no-cache bash ca-certificates curl python3 tzdata libcap \
|
||||
; ! [[ "$(uname -m)" == x86_64 ]] \
|
||||
|| apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing hardened-malloc==11-r0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD /bin/bash
|
||||
|
||||
|
||||
# build virtual env (intermediate)
|
||||
FROM system as build
|
||||
|
||||
ARG MAILU_DEPS=prod
|
||||
ARG SNUFFLEUPAGUS_VERSION=0.9.0
|
||||
|
||||
ENV VIRTUAL_ENV=/app/venv
|
||||
|
||||
COPY requirements-build.txt ./
|
||||
|
||||
RUN set -euxo pipefail \
|
||||
; apk add --no-cache py3-pip \
|
||||
; python3 -m venv ${VIRTUAL_ENV} \
|
||||
; ${VIRTUAL_ENV}/bin/pip install --no-cache-dir -r requirements-build.txt \
|
||||
; apk del -r py3-pip \
|
||||
; rm -f /tmp/*.pem
|
||||
|
||||
COPY requirements-${MAILU_DEPS}.txt ./
|
||||
COPY libs/ libs/
|
||||
|
||||
ENV \
|
||||
PATH="${VIRTUAL_ENV}/bin:${PATH}" \
|
||||
CXXFLAGS="-g -O2 -fdebug-prefix-map=/app=. -fstack-protector-strong -Wformat -Werror=format-security -fstack-clash-protection -fexceptions" \
|
||||
CFLAGS="-g -O2 -fdebug-prefix-map=/app=. -fstack-protector-strong -Wformat -Werror=format-security -fstack-clash-protection -fexceptions" \
|
||||
CPPFLAGS="-Wdate-time -D_FORTIFY_SOURCE=2" \
|
||||
LDFLAGS="-Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now" \
|
||||
SNUFFLEUPAGUS_URL="https://github.com/jvoisin/snuffleupagus/archive/refs/tags/v${SNUFFLEUPAGUS_VERSION}.tar.gz"
|
||||
|
||||
RUN set -euxo pipefail \
|
||||
; machine="$(uname -m)" \
|
||||
; deps="build-base gcc libffi-dev python3-dev" \
|
||||
; [[ "${machine}" != x86_64 ]] && \
|
||||
deps="${deps} cargo git libretls-dev mariadb-connector-c-dev postgresql-dev" \
|
||||
; apk add --virtual .build-deps ${deps} \
|
||||
; [[ "${machine}" == armv7* ]] && \
|
||||
mkdir -p /root/.cargo/registry/index && \
|
||||
git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \
|
||||
; pip install -r requirements-${MAILU_DEPS}.txt \
|
||||
; curl -sL ${SNUFFLEUPAGUS_URL} | tar xz \
|
||||
; cd snuffleupagus-${SNUFFLEUPAGUS_VERSION} \
|
||||
; rm -rf src/tests/*php7*/ src/tests/*session*/ src/tests/broken_configuration/ src/tests/*cookie* src/tests/upload_validation/ \
|
||||
; apk add --virtual .build-deps php81-dev php81-cgi php81-simplexml php81-xml pcre-dev build-base php81-pear php81-openssl re2c \
|
||||
; pecl install vld-beta \
|
||||
; make -j $(grep -c processor /proc/cpuinfo) release \
|
||||
; cp src/.libs/snuffleupagus.so /app \
|
||||
; rm -rf /root/.cargo /tmp/*.pem /root/.cache
|
||||
|
||||
# base mailu image
|
||||
FROM system
|
||||
|
||||
COPY --from=build /app/venv/ /app/venv/
|
||||
COPY --chown=root:root --from=build /app/snuffleupagus.so /usr/lib/php81/modules/
|
||||
RUN setcap 'cap_net_bind_service=+ep' /app/venv/bin/gunicorn 'cap_net_bind_service=+ep' /usr/bin/python3.10
|
||||
|
||||
ENV \
|
||||
VIRTUAL_ENV=/app/venv \
|
||||
PATH="/app/venv/bin:${PATH}" \
|
||||
LD_PRELOAD="/usr/lib/libhardened_malloc.so" \
|
||||
ADMIN_ADDRESS="admin" \
|
||||
FRONT_ADDRESS="front" \
|
||||
SMTP_ADDRESS="smtp" \
|
||||
IMAP_ADDRESS="imap" \
|
||||
OLETOOLS_ADDRESS="oletools" \
|
||||
REDIS_ADDRESS="redis" \
|
||||
ANTIVIRUS_ADDRESS="antivirus" \
|
||||
ANTISPAM_ADDRESS="antispam" \
|
||||
WEBMAIL_ADDRESS="webmail" \
|
||||
WEBDAV_ADDRESS="webdav"
|
@ -0,0 +1,20 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
tmp
|
||||
|
||||
*.bak
|
||||
*~
|
||||
.*.swp
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.egg-info/
|
||||
|
||||
.build
|
||||
.env*
|
||||
.venv
|
||||
|
||||
*.code-workspace
|
||||
|
||||
build/
|
@ -0,0 +1,7 @@
|
||||
This project is open source, and your contributions are all welcome. There are mostly three different ways one can contribute to the project:
|
||||
|
||||
1. use Podop, either on test or on production servers, and report meaningful bugs when you find some;
|
||||
2. write and publish, or contribute to mail distributions based on Podop, like Mailu;
|
||||
2. contribute code and/or configuration to the repository (see [the development guidelines](https://mailu.io/contributors/guide.html) for details);
|
||||
|
||||
Either way, keep in mind that the code you write must be licensed under the same conditions as the project itself. Additionally, all contributors are considered equal co-authors of the project.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue