forked from Silverfish/proton-bridge
Compare commits
1141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0800aeea50 | |||
| f4ddf43ac7 | |||
| da0f51ce5f | |||
| d711d9f562 | |||
| fe39d23cf8 | |||
| dbb84f2ae2 | |||
| a2c1da9748 | |||
| b8cc71fdd8 | |||
| 1949e89053 | |||
| ae5469fc81 | |||
| 105ea4de0d | |||
| b21d126ab0 | |||
| 7bc7a5e7b3 | |||
| edbc7d0e3d | |||
| b7b1043b88 | |||
| 0641c63377 | |||
| 74a990c69a | |||
| e340e9f845 | |||
| 082849dc6c | |||
| 6878b3b5e0 | |||
| 16245a372e | |||
| 28b0dbd051 | |||
| 0e6df4ce73 | |||
| a4772ee4e0 | |||
| ef779a23c1 | |||
| dd2448f35a | |||
| 3f78f4d672 | |||
| 5fbe94c559 | |||
| 80d556343e | |||
| 612d1054db | |||
| acf2fc32c4 | |||
| af01c63298 | |||
| 2e98d64f94 | |||
| cdcdd45bcf | |||
| b3e2a91f56 | |||
| 7d9753e2da | |||
| f1aef383b7 | |||
| 6647231278 | |||
| 531368da86 | |||
| 0c21925939 | |||
| 19a445e73a | |||
| 96e0070ed2 | |||
| 516ff5206d | |||
| 4d2b328589 | |||
| 810be2d423 | |||
| e3d0334b6f | |||
| fb523e5573 | |||
| cb8d1a2389 | |||
| 84f0a6722a | |||
| c3a495facd | |||
| 9cdc40ca05 | |||
| 7021b1c2ea | |||
| 607d9df8a9 | |||
| 93396145dc | |||
| 7457fb06d2 | |||
| bee2642aec | |||
| b481ce2203 | |||
| 040d887aae | |||
| 3710dff0cd | |||
| f5bc6ad1f0 | |||
| e8a95e26f6 | |||
| ebe54ca92e | |||
| ff7e45f395 | |||
| 79c63f5785 | |||
| 3ca9e625f5 | |||
| 5b874657cb | |||
| bfe67f3005 | |||
| 99e6f00aaa | |||
| ac9ab8ab32 | |||
| f04350c046 | |||
| ed1b65731a | |||
| d12928b31c | |||
| 1ea06a95b7 | |||
| e290cd308b | |||
| 3d53bf7477 | |||
| 84c0b907d7 | |||
| b30455b641 | |||
| db9902e70b | |||
| f1f63c1d03 | |||
| 81a3c2aba8 | |||
| bbfc9beb04 | |||
| c4dba09ee6 | |||
| a5435eb1da | |||
| 54c56efdfa | |||
| fc64dbec59 | |||
| 5d3f084a2b | |||
| 606d6c0e3e | |||
| 9fbb6b4ca5 | |||
| 8688277ee6 | |||
| 63eb67760e | |||
| cffab028b2 | |||
| 8ea712b052 | |||
| ff0615167b | |||
| e2b361b9a6 | |||
| 1c6bbf1fae | |||
| e7713fa785 | |||
| 28ae54b5ca | |||
| 00aff40160 | |||
| ab289e6e01 | |||
| a28dc9f2f3 | |||
| 8a859082cd | |||
| 1d972835ff | |||
| 8469e0a661 | |||
| 6ea970bf97 | |||
| a05b90e803 | |||
| 239ad8b946 | |||
| d9fdbb35bc | |||
| 5769fb9466 | |||
| a4020cebd4 | |||
| 7a8760e2ef | |||
| 9552e72ba8 | |||
| c692c21b87 | |||
| bb15efa711 | |||
| e94d3be12d | |||
| 66569f71a0 | |||
| 9bfa79455e | |||
| 67e802e3a0 | |||
| 8a5e2007f6 | |||
| 5b92945626 | |||
| 4a8a7ef093 | |||
| 2cfda14b1a | |||
| 312993e08e | |||
| b1110b04c9 | |||
| d2bc60d9cb | |||
| 1d8f6c75c8 | |||
| 06daaf8d9f | |||
| cb436fff63 | |||
| 921a44f1a3 | |||
| d35af6b686 | |||
| 4cb938c57f | |||
| 232e98d812 | |||
| 6fadbde4a6 | |||
| d2fbbc3e25 | |||
| 1c7c342e19 | |||
| 8e49c84a12 | |||
| 754d80d097 | |||
| 63e272e270 | |||
| 54859a34b2 | |||
| 9b1feed68b | |||
| c9b6cc162b | |||
| bf3c90b8e9 | |||
| 8d63fb2301 | |||
| 7953306cc8 | |||
| 37352d44d2 | |||
| 2a1aeb208d | |||
| 94fbe260e4 | |||
| 6d4937222e | |||
| e33bad7bf1 | |||
| 70fdc91aff | |||
| bde8e45b37 | |||
| 6cb2d944d0 | |||
| cf0f59afc0 | |||
| 65d8fbbf31 | |||
| d919c0accf | |||
| 0ca07066db | |||
| 7fa1948c21 | |||
| 413ab1fc1e | |||
| 45c2102ff7 | |||
| 97fc964467 | |||
| bfde96dc88 | |||
| fdb5c0cbee | |||
| 5b4c6870b5 | |||
| a433da8782 | |||
| 5df95566b7 | |||
| 164fb23653 | |||
| 374194c13b | |||
| 1cd35defe5 | |||
| 56aa497b9d | |||
| 773a230d14 | |||
| 76d257af21 | |||
| 856efec886 | |||
| 46fd1d5a76 | |||
| f565fc4f69 | |||
| 2895f42a64 | |||
| 5751166ebc | |||
| e63afd3910 | |||
| 9b1daa0373 | |||
| 89bb7b6389 | |||
| 31670ad9eb | |||
| fb32d652bc | |||
| 346988e604 | |||
| 43df20c25d | |||
| 25ebcffde3 | |||
| b8ae5be58c | |||
| 26a3385f4e | |||
| dc002959eb | |||
| 8703faf345 | |||
| 3ac59d6943 | |||
| 8f5bd37aee | |||
| 5c69af4418 | |||
| 416f696863 | |||
| 789c1cc816 | |||
| 58736dd254 | |||
| a057138880 | |||
| 76087f1749 | |||
| 83935f3a03 | |||
| b93c10ad47 | |||
| 3309137b80 | |||
| 88c4737ba4 | |||
| e5db9b1ccc | |||
| 6e2e622a2f | |||
| 3a66063938 | |||
| 120ddbbcbb | |||
| 39b31abef8 | |||
| ebeca394c7 | |||
| 2206cb3f12 | |||
| cfd07cf893 | |||
| 2e2648fcd5 | |||
| 3070912416 | |||
| 51722eb1a4 | |||
| 5950eff083 | |||
| 5c67cc2e76 | |||
| 01db488caa | |||
| 6cbef1d786 | |||
| dd9a819ea2 | |||
| 401e56224b | |||
| 1ee52f0f55 | |||
| 9efaf9184c | |||
| a8f270405f | |||
| 38606888fe | |||
| 1b22c32ef9 | |||
| 7a1c7e8743 | |||
| 9449177553 | |||
| bbcedc655a | |||
| 40c97ab19e | |||
| 50dd046b82 | |||
| 7d13c99710 | |||
| 6d7c21b2c9 | |||
| f7434109be | |||
| 414d74d06a | |||
| 110cdbf3ae | |||
| ec4ceb4552 | |||
| ef62704030 | |||
| eaba6b6363 | |||
| e1723fc24b | |||
| 2073513d5e | |||
| 36f7d9672f | |||
| ef183e0758 | |||
| 0d2a803711 | |||
| 06b5276981 | |||
| b2d61da41f | |||
| e51c81fc03 | |||
| 26897f06c4 | |||
| 5ca9a7db37 | |||
| b34f5d072f | |||
| eeb514cc81 | |||
| 650ad49ab0 | |||
| 0e5715c4e3 | |||
| b0f1c3d4c5 | |||
| ba935a6cce | |||
| 1370ff78c5 | |||
| 109c15410a | |||
| 3210709810 | |||
| 8fd988d7c5 | |||
| bf89d548d3 | |||
| 51229cbb68 | |||
| 36c5c37dac | |||
| 5a434fafbc | |||
| ea1c2534df | |||
| 1cafbfcaaa | |||
| 2d44ccaee0 | |||
| 96517b7fb1 | |||
| bc381407a7 | |||
| ddc5e775b9 | |||
| ea26188dc0 | |||
| 159e1cee7d | |||
| 4394ad0e9b | |||
| 856bdd1321 | |||
| ff288145df | |||
| 83bbdbd63e | |||
| fa430ee0fb | |||
| 0303ba38e8 | |||
| 2a78b5c144 | |||
| a00b3cdb92 | |||
| 8d3e04679f | |||
| 21ff7b4b97 | |||
| 4ea161f7ad | |||
| dc584ea29b | |||
| 4a01c46aed | |||
| e8d9534b9c | |||
| 96904b160f | |||
| b535be72f8 | |||
| 40f2d8b30f | |||
| 95a1acec0d | |||
| 5ff074cc49 | |||
| 4f0660bb8c | |||
| 708184439e | |||
| b8a33b9618 | |||
| 1c385d5c9b | |||
| 96773f3225 | |||
| 0f320dbd80 | |||
| 6cb233473a | |||
| 1ac4e70115 | |||
| 07f93d276b | |||
| d29571fb01 | |||
| d6000d025e | |||
| 09ef3b20db | |||
| 405331d59b | |||
| eff7df2136 | |||
| 5823e3a99f | |||
| 26d866bbbd | |||
| d3f7be059d | |||
| b52706a3ca | |||
| aebe7baed0 | |||
| ef31e2917c | |||
| 9eea26459a | |||
| 5747b85543 | |||
| ff78a23084 | |||
| 2a95e1ab41 | |||
| ab76cab533 | |||
| dda2a5d01a | |||
| c2afb42fd4 | |||
| 1d53044803 | |||
| d3f8297eb4 | |||
| b02203e3d3 | |||
| 5c7e4e04f9 | |||
| d7dadd7578 | |||
| ab9a758d63 | |||
| cb0935be96 | |||
| 441b388f62 | |||
| cdbcd30d15 | |||
| acc7ca8d4a | |||
| 42e1dd4c41 | |||
| 4cbd3ca832 | |||
| de0b6c0737 | |||
| 1c344211d1 | |||
| c11a87c16a | |||
| 3bf4282037 | |||
| 0c212fbef4 | |||
| 48d1ca1e72 | |||
| 52addb2582 | |||
| 742d9eeef3 | |||
| 55a9d4973c | |||
| 8402657108 | |||
| 8a6f96f9f2 | |||
| 56c53e9188 | |||
| bb67d95669 | |||
| 50acc0dcfb | |||
| e9c73c2d0d | |||
| 07c03c6920 | |||
| f4958b9b53 | |||
| 76f2e7fdb9 | |||
| c0992e8801 | |||
| cf3abaa96f | |||
| e422b28bc3 | |||
| a1a5ffba5d | |||
| f8b86a76dd | |||
| ab1281ceee | |||
| 0ab0f2f4ff | |||
| 09d87023f1 | |||
| 139ad75394 | |||
| c8cf90abfe | |||
| 5d4f8f7d40 | |||
| ea26dc0e97 | |||
| 8d346ea511 | |||
| 44df3cfd4a | |||
| 683458e264 | |||
| 36651698cb | |||
| 0c7e17701f | |||
| 86cd2437aa | |||
| 53f5f9aa43 | |||
| c849762445 | |||
| 32f2c72575 | |||
| 958e1280d7 | |||
| df09d6d221 | |||
| e0875dc928 | |||
| b3a5270bdc | |||
| f617a44d28 | |||
| 75ed3ca660 | |||
| 69f3029430 | |||
| 1203709ab9 | |||
| 15c18189d3 | |||
| a9e95f618b | |||
| 272f9cf59b | |||
| 6e86c95640 | |||
| 81afc5fb1f | |||
| 53ea5e9adc | |||
| 6f420f9098 | |||
| 65846ff40f | |||
| 43f7a989be | |||
| 452d3068f0 | |||
| 69190daf3f | |||
| f57a40677e | |||
| 2d6f42e0b5 | |||
| bccf31501d | |||
| 9b546b5412 | |||
| f48a60d58c | |||
| 0a51c7a6b0 | |||
| 7355c7dfd6 | |||
| bb5a91ee6d | |||
| ca5f7ce9f6 | |||
| ad31e6a9c5 | |||
| 9ef7d133c0 | |||
| 83b842b19d | |||
| df02e39fe1 | |||
| a35c8424a3 | |||
| 5d207810bd | |||
| 6c9d96d5e1 | |||
| 0fc41d1966 | |||
| dd5e745e37 | |||
| c8f0d7f32a | |||
| bd986901c3 | |||
| cdc19492ee | |||
| 635b2a4891 | |||
| e5bac33a04 | |||
| 7b96a07cf5 | |||
| 87e79fdcba | |||
| 03c3404044 | |||
| fa794a982b | |||
| cab32d5d5a | |||
| 8e5a892c45 | |||
| 50dc5c4085 | |||
| 3b58078595 | |||
| 7689139cb3 | |||
| 6269b1ab88 | |||
| 79524185a8 | |||
| 635b81314a | |||
| 4c76e35a2d | |||
| b0ac20425e | |||
| 21a56a0725 | |||
| c57b52ef23 | |||
| 44e103edd5 | |||
| 970de4c205 | |||
| a189c35899 | |||
| c9323a40c8 | |||
| 7a192d50db | |||
| a5a9bd762d | |||
| dd7db00f74 | |||
| a6c20f698c | |||
| f7252ed40e | |||
| 66c30716ea | |||
| 06d7fdf26f | |||
| 8aee732fe8 | |||
| 47233752f5 | |||
| cb6e51531b | |||
| 0b9b886039 | |||
| 1fa0d77b10 | |||
| efbe84964f | |||
| aa77a67a1c | |||
| 78f7cbdc79 | |||
| a731237701 | |||
| f557666b4d | |||
| 5f89e85139 | |||
| 9fb922d18f | |||
| 884c6ed932 | |||
| db77bd4983 | |||
| 85bfcf7158 | |||
| 09a5f8ac0f | |||
| 81f81a63e8 | |||
| e724fafe2f | |||
| cba73997cd | |||
| 29f44fc312 | |||
| 41c125f65e | |||
| 0a555bf767 | |||
| 24331f9715 | |||
| a5500629e5 | |||
| a46533dcf2 | |||
| 12183fbf05 | |||
| c90248920a | |||
| ccb9b7f81c | |||
| 78c0651661 | |||
| d72980e443 | |||
| b24937b666 | |||
| 5ca9ec6674 | |||
| 4ee6da4baa | |||
| e8ce8942be | |||
| 9d0d3708af | |||
| 4c908aac7c | |||
| 6eb1878f66 | |||
| 826dc2e5c3 | |||
| e6ab874308 | |||
| 20b188368a | |||
| ded4f370dc | |||
| 519a6ecdb7 | |||
| c35344d6f1 | |||
| a9865976a3 | |||
| 9a96588afb | |||
| 1f2c573803 | |||
| cc33423c1f | |||
| 1b95c290f1 | |||
| 9b88778c43 | |||
| ae4705ba70 | |||
| 243ddf47ab | |||
| 80d729e3e5 | |||
| 3d64c5f894 | |||
| c4bcc38c53 | |||
| 2c2f816f3a | |||
| 86e115b2f3 | |||
| 1a2783a63b | |||
| cbbab71f5c | |||
| 80add80be2 | |||
| 84adbbc461 | |||
| 75811d22e8 | |||
| e26c7683d2 | |||
| f0e2688a8e | |||
| 06639ff6cd | |||
| 716de7f45a | |||
| 750de0cd31 | |||
| 823ca4d207 | |||
| a187747c7c | |||
| 11ebb16933 | |||
| 0048767022 | |||
| c4b75c6f34 | |||
| 32448063dc | |||
| 86bde91958 | |||
| 90c34406ba | |||
| d7b71aceda | |||
| 25a787529b | |||
| f82965b825 | |||
| f1cf4ee194 | |||
| 5136919c36 | |||
| 334a256638 | |||
| da528f2d9b | |||
| 1b0f930471 | |||
| 09c523e2d2 | |||
| 0b35f41ffd | |||
| 7be1a8ae8a | |||
| bdbf1bdd76 | |||
| 776976a8a2 | |||
| 040ddadb7a | |||
| 11f6f84dd6 | |||
| 82efa16d65 | |||
| 110286b81c | |||
| bc66841cdc | |||
| 8d028966c7 | |||
| d120bbeffc | |||
| eda49483e2 | |||
| 3ef526333a | |||
| f454f1a74f | |||
| e97a4d8847 | |||
| cb8174dbfd | |||
| cfca429067 | |||
| 1ffb9089ba | |||
| c0fc23d7cd | |||
| 4f8ecd598f | |||
| 65365281eb | |||
| c05dfb36d3 | |||
| 42178806d1 | |||
| e34050282e | |||
| b2830b39e0 | |||
| 7997ad2b93 | |||
| efb6ba0f1b | |||
| 80194ad797 | |||
| cda6b2a728 | |||
| 44bde86fde | |||
| 6cb52cacc9 | |||
| 8248491833 | |||
| fbdede28f0 | |||
| 66bc911652 | |||
| c860741ffa | |||
| 80d743afec | |||
| ac9857a965 | |||
| 0e9fd46a5c | |||
| 305d180a5f | |||
| c4f80103b6 | |||
| 0d57e3645a | |||
| 0afdc31f96 | |||
| 91de6e001e | |||
| 908ed3e723 | |||
| 7411073c08 | |||
| 7d838375bb | |||
| f545f30ec0 | |||
| 6579cdfc7f | |||
| 40c48ba804 | |||
| 1c2cb4f439 | |||
| 650dac37a7 | |||
| 55275b23ee | |||
| bce69e1a1b | |||
| f1917ad0de | |||
| 300d243331 | |||
| 6ea6d54af6 | |||
| c1b486a7eb | |||
| 454c9e1534 | |||
| eaa673c4e4 | |||
| 9c389e3007 | |||
| 7e9a5934c5 | |||
| cc17366c1c | |||
| 62f6db35db | |||
| be422001e8 | |||
| b2eb35592f | |||
| a3b26431ce | |||
| f460323cc5 | |||
| 26694d3bd8 | |||
| e96713a998 | |||
| 6359b8639e | |||
| 6c9d5ccd4a | |||
| 234554b459 | |||
| 6df5a82364 | |||
| ac75410657 | |||
| 238929c3ec | |||
| 1f79e3b0a7 | |||
| f5af2afce5 | |||
| 9482bea8af | |||
| a55572e5b3 | |||
| 098eb7cb7a | |||
| 68334e3bb8 | |||
| 124231c3c7 | |||
| f591af2cbd | |||
| ff11d20d9c | |||
| 72911235c5 | |||
| 60de00c73f | |||
| 4e080b59d3 | |||
| bac4b90c1d | |||
| ea47c9aa1c | |||
| a91d9762db | |||
| 6fb11d69f9 | |||
| 1eab3296d1 | |||
| 4352154b84 | |||
| 03cf601921 | |||
| 9f13301613 | |||
| 650158ea8a | |||
| 552fc2700f | |||
| 582afa1451 | |||
| c43739a7ef | |||
| 720f662afe | |||
| e9488d12ee | |||
| 9a87b155ba | |||
| f6a1cd9b64 | |||
| 7b7c9093ce | |||
| fa4c0ec823 | |||
| 08af1da966 | |||
| ed8475dacf | |||
| e91cdca6f3 | |||
| c267168cb7 | |||
| 58b45d8458 | |||
| b77512dfd9 | |||
| bdc6542970 | |||
| c942a44f6a | |||
| 55081fa59b | |||
| cc1d0e803b | |||
| b7a2371220 | |||
| ae65385c38 | |||
| ab70e85f1c | |||
| ff57eb2b43 | |||
| e78cb8089b | |||
| 09eef64514 | |||
| a2c2710760 | |||
| c4abb14ae6 | |||
| 38a0cdb4ab | |||
| c587dfc0dc | |||
| 7a090ffcc9 | |||
| 6ab49367ba | |||
| a06fd31f49 | |||
| 016319784e | |||
| 12f9fb03c3 | |||
| 419a4427d7 | |||
| e434a4fd0c | |||
| d68b163c20 | |||
| 71a559ee4c | |||
| ac00ef1b64 | |||
| 1e9a77c7b2 | |||
| 4aaa555c9b | |||
| a8dd52800e | |||
| fab063f194 | |||
| 5c606aee73 | |||
| 51315d5d2b | |||
| cde9c19f71 | |||
| 4902898880 | |||
| c46b3245b8 | |||
| 3afd94c61d | |||
| ade2fd9403 | |||
| 89632b7acd | |||
| b8cf193911 | |||
| fb4d34ef5b | |||
| 7f7e360cd7 | |||
| 802f7dbc67 | |||
| fc06665d2b | |||
| c4dc829e6d | |||
| bfaf9765ae | |||
| a702e19dff | |||
| 0eab1c0c2b | |||
| 11f55b59a9 | |||
| 172b59c756 | |||
| 8564d3a483 | |||
| 1d595b933a | |||
| 39cd165bd1 | |||
| 0cef181432 | |||
| 262c4c5d95 | |||
| e343d1f1af | |||
| 84a771d9fe | |||
| f6741a9b58 | |||
| bc5de2b884 | |||
| 7d54e6907d | |||
| 4b52d998db | |||
| aa72fd641d | |||
| ebe45d5abe | |||
| b6eb5a1b13 | |||
| 9c25f56fe6 | |||
| bb99695e68 | |||
| 35f0e081a5 | |||
| 40dc17aea5 | |||
| 5fee2f707b | |||
| d6304b087a | |||
| 21f833ea11 | |||
| 900caec09e | |||
| 9fc9f5ad9f | |||
| d8ccc6c05d | |||
| 4e3ad4f7fa | |||
| 8c958cdc2f | |||
| afef730870 | |||
| 0ef9c9c9c9 | |||
| f691795ca1 | |||
| 73bb0ed03e | |||
| faf3780eee | |||
| 98031d296e | |||
| d08b3fcca4 | |||
| 6adb440b84 | |||
| a3e07428b5 | |||
| dfd85f7ed3 | |||
| 4b5edd62d0 | |||
| fb4a0e77af | |||
| edac2419f9 | |||
| 6ba8052a1e | |||
| 51288791c0 | |||
| 9798cdec5c | |||
| 9ae899f420 | |||
| 51aafe3266 | |||
| 8fd09547fa | |||
| aba94a9353 | |||
| e6dfb814fd | |||
| bfa53b1619 | |||
| b9c54e9276 | |||
| 71d7deb3b1 | |||
| e606d98664 | |||
| 36342299c7 | |||
| a05f93debd | |||
| c438704648 | |||
| 0417e495ae | |||
| bda158d6c6 | |||
| ad02c71ad6 | |||
| eee2c73a61 | |||
| 5630b7d2e6 | |||
| 2ee4893325 | |||
| 01aa19edff | |||
| a0db1645f2 | |||
| 324593596a | |||
| b51d85e768 | |||
| bd47303074 | |||
| fdae8cb729 | |||
| 333daa05c5 | |||
| 6a6ead8e6d | |||
| 543c35041d | |||
| 50709acc5f | |||
| 994cec196c | |||
| 910060a14c | |||
| 38b13d710c | |||
| 06f710a9b1 | |||
| fbbd0245de | |||
| 7002806999 | |||
| c49b42060e | |||
| 21b20ac420 | |||
| f2a8990972 | |||
| 8db30a89c0 | |||
| 449b580056 | |||
| 38397accf5 | |||
| 5d49cf2c09 | |||
| d8fa2fb3e3 | |||
| 2f7f898cee | |||
| ae7621ed6a | |||
| f3fb10fb2d | |||
| 9241e5317f | |||
| 3ebd2f53f4 | |||
| 59a944a06c | |||
| f9a0c35daa | |||
| e9629aca47 | |||
| 8d53ee855b | |||
| ec0db47f32 | |||
| 7383d65cb2 | |||
| 3ef3ab72ed | |||
| 00adb8bc22 | |||
| d3fc9a50f6 | |||
| 4adc6d60b9 | |||
| d88bee68c6 | |||
| 67b5e7f96a | |||
| b250d49af8 | |||
| 0f621d0aad | |||
| 6ddaa94bc0 | |||
| fed503501d | |||
| ce5a559926 | |||
| 3b297fa37b | |||
| 95741c6d63 | |||
| ad4e853a8a | |||
| f4631c4bc9 | |||
| c000ee8a3c | |||
| 3ddd88e127 | |||
| 54b209f9e1 | |||
| 4e7a669260 | |||
| 8093bbf5f6 | |||
| 7bb925b6d7 | |||
| d6760d6f50 | |||
| 2191dc70dc | |||
| ff44a3d6df | |||
| b0c3faa228 | |||
| 3928ed08f6 | |||
| c7ae239350 | |||
| 7d51e9123d | |||
| 6fcea2ad83 | |||
| c1918e2b1b | |||
| 098f294cac | |||
| a6f5cc870c | |||
| 60a779c653 | |||
| a8cb0012e1 | |||
| c84919faae | |||
| 3735d4b327 | |||
| 7330406752 | |||
| 1323229362 | |||
| fd100eecc2 | |||
| a11559fe58 | |||
| 7d8e71c9ea | |||
| 8b80938e49 | |||
| 0a53dc1da7 | |||
| 9bbeabcf50 | |||
| de5fd07a22 | |||
| ec92c918cd | |||
| 9f59e61b14 | |||
| 86fd7961a1 | |||
| 9756a7b51f | |||
| aa957f3314 | |||
| 7124e7c9a0 | |||
| 421da99cd8 | |||
| ef1940d227 | |||
| 579e996d3a | |||
| 7ba523393c | |||
| 0a0bcd7b90 | |||
| f1fb525dc8 | |||
| ee87e60239 | |||
| 4adc05d354 | |||
| 86fbf4415a | |||
| babb4412ae | |||
| 74203e07a4 | |||
| 2166224e91 | |||
| 60df01eece | |||
| 58df3312c1 | |||
| f1989193c0 | |||
| 4e7acd9091 | |||
| 4c94606e20 | |||
| 3ca5d0af71 | |||
| 9425e091d8 | |||
| ee5e87fe85 | |||
| dbd156a272 | |||
| bb770f3871 | |||
| b1ad0ab6dc | |||
| 03c3e95b34 | |||
| b63b56960e | |||
| a8250ff65e | |||
| 2d6e0c66a5 | |||
| d8bf2cc25e | |||
| ed05823c78 | |||
| ec351330f1 | |||
| 4f49c87bc6 | |||
| 02ca6428b5 | |||
| b3d0dfc60b | |||
| 622b76b57b | |||
| dd7c81ca4b | |||
| f71a89c265 | |||
| e1dff67c10 | |||
| 31de358bfd | |||
| 212eac0925 | |||
| e36c2b3d6e | |||
| 8b33d56b59 | |||
| 48274ee178 | |||
| 4273405393 | |||
| 3a85de2f3c | |||
| a3aafabde3 | |||
| 30c1c14505 | |||
| 8164c41728 | |||
| 1820af5021 | |||
| b57ca1506d | |||
| 7eaf1655b2 | |||
| b4ea667858 | |||
| 8f45fe823a | |||
| f627151d04 | |||
| 7c232b1331 | |||
| 7be46a4740 | |||
| b57c7abe92 | |||
| c86c428718 | |||
| ed6e17a0ab | |||
| c3454360fc | |||
| 182dab18a6 | |||
| 13c8a98389 | |||
| 05a2c9d254 | |||
| d926dd3806 | |||
| 7cc2f3361d | |||
| c496d6c71c | |||
| 7fc907a874 | |||
| b953468af2 | |||
| 9f4caa4948 | |||
| 86630ce137 | |||
| 2c9477d65c | |||
| 34c002ff68 | |||
| f03688ba72 | |||
| 8c0bb22de3 | |||
| 53c2cbcaee | |||
| 07339aff21 | |||
| 3ca56cfab3 | |||
| 59cf5e890b | |||
| 70950e0048 | |||
| 2781f06549 | |||
| febc994cec | |||
| 21ffde316d | |||
| 2aa4e7c9da | |||
| 9058544f5c | |||
| 227bbf1c03 | |||
| 667998c207 | |||
| 1d426e621c | |||
| 6e4dcdb93b | |||
| 20f35edc83 | |||
| 26cf684fb8 | |||
| 2a4cb6a916 | |||
| caa4a5cbdb | |||
| 91900a7942 | |||
| 04a7a81e27 | |||
| 53d5619c51 | |||
| f793b71569 | |||
| 8aec11a634 | |||
| 30651a531b | |||
| 91ab77dce9 | |||
| 69aa784d32 | |||
| 825031e052 | |||
| ee4a8939d5 | |||
| 89117bbd59 | |||
| 9e4310712c | |||
| 0b35b275d3 | |||
| d6acb0fb19 | |||
| 2db5a04e7a | |||
| d7bfee2414 | |||
| 4a6460f3ed | |||
| 3b1c3b9d44 | |||
| 60256fd076 | |||
| c6f1f159f3 | |||
| 82af4e01bc | |||
| 9ad5f74409 | |||
| 10cf153678 | |||
| 2b09796d1c | |||
| 5ba07db7e3 | |||
| ad0d4ebd36 | |||
| 9f3c14ab1e | |||
| 74cf5d422b | |||
| dcf694588c | |||
| a2b9fc3dee | |||
| 761c16d8cd | |||
| 810705ba01 | |||
| c15917aba4 | |||
| 51cbb91513 | |||
| f8bfbaf361 | |||
| 3e878058e7 | |||
| 9c1b9e8df2 | |||
| 065dcd4d47 | |||
| fb44de4f18 | |||
| 00256fafe8 | |||
| 671db7c516 | |||
| e9f20aee7a | |||
| 8534da98ea | |||
| 265af2d299 | |||
| 82c388a0dd | |||
| 89112baf96 | |||
| 5007d451c2 | |||
| 4775fb4b22 | |||
| 94ed09b437 | |||
| 57962e5757 | |||
| 8a5c8eaf6e | |||
| a75a72a2b9 | |||
| 038eb6d243 | |||
| 2bd8f6938a | |||
| 889f3286b5 | |||
| c821d02f67 | |||
| 9dfdd07f7a | |||
| 0b796f4401 | |||
| a741ffb595 | |||
| cf8284a489 | |||
| ec2a4f9111 | |||
| 6a9f6a173a | |||
| 54c013012e | |||
| cac0cf35f6 | |||
| 30029f489e | |||
| 968a01053f | |||
| 2faeebe9e7 | |||
| f6727a56d2 | |||
| 4c24c004db | |||
| 571133f2ff | |||
| 0207fa04f1 | |||
| 98fdf45fa3 | |||
| 21b3a4bca3 | |||
| 7225fc31da | |||
| eca4810f19 | |||
| 249658c05b | |||
| da82d7a107 | |||
| 08dab2d115 | |||
| 13db1b0db8 | |||
| c1921a811b | |||
| 6f914a4973 | |||
| 473be3d485 | |||
| d7fd39503f | |||
| b4b66f94ec | |||
| 0823d393ed | |||
| cbd36184bd | |||
| 465f754803 | |||
| 8b9265ad96 | |||
| 5f930c262c | |||
| afa95d4799 | |||
| 2fa7c97f39 | |||
| 2b75fcf773 | |||
| 76bdc21fef | |||
| cdff2ef792 | |||
| a740a8f962 | |||
| d1f1c390f6 | |||
| 1c88ce3cc0 | |||
| c4ef1a24c0 | |||
| a79fce907e | |||
| c9d496956c | |||
| 31dce41276 | |||
| c6576dfc4b | |||
| 9048b14fdb | |||
| 43100d11bf | |||
| 4876314cf5 | |||
| 2f75131710 | |||
| 1e09fd6662 | |||
| 48f2c56caa | |||
| 20d83dd476 | |||
| 9c6be78b4c | |||
| 0a8e71771e | |||
| 29d1c7bccd | |||
| ca1996a670 | |||
| ab1c1c474a | |||
| d7cac8a8f0 | |||
| 63bc87cc86 | |||
| 232875d5cc | |||
| f3c5e300cd | |||
| 29072f0285 | |||
| 5ea53ea5c0 | |||
| 40aca0fe73 | |||
| f4a2fb9687 | |||
| 367c505444 | |||
| 3bd39b3ea5 | |||
| e89dcb2cca | |||
| ad65bdde9d | |||
| 34cd611a8b | |||
| d82b71de89 | |||
| 8894a982f2 | |||
| 2cb2ca15c7 | |||
| db41645159 | |||
| a74d1ce9ca | |||
| 2e832520e6 | |||
| 62285a141e | |||
| c3d5a0b8f8 | |||
| a36dbbf422 | |||
| 4cf23bb2e6 | |||
| e2c1f38ed3 | |||
| 5ec1da34b4 | |||
| ea11c1046a | |||
| df40f27069 | |||
| 76d732f247 | |||
| 219400de8d | |||
| 8901d83c94 | |||
| 0c8d4e8dd8 | |||
| a955dcbaa9 | |||
| fbac5134ca | |||
| 45ec6b6e74 | |||
| dd29ff4731 | |||
| 8b94a28e00 | |||
| 60100ad7f0 | |||
| 62a50fd7fc | |||
| b17bdad864 | |||
| 52daa165a2 | |||
| 4c5ba04822 | |||
| 79c2523585 | |||
| f14ad8b3fa | |||
| 590fdacba3 | |||
| 342a2a5568 | |||
| e382687168 | |||
| 4577a40b1e | |||
| c0aacb7d62 | |||
| c8065c8092 | |||
| ce03bfbf0f | |||
| 0182e2c0bc | |||
| e464e11ab9 | |||
| d7ff54d679 | |||
| 4aa1091f62 | |||
| 6d024d2055 | |||
| c86cdf737f | |||
| 7e36b215fe | |||
| badebbef9f | |||
| 5e072c3282 | |||
| 048c3a900c | |||
| 88a9fe410c | |||
| cf32b84257 | |||
| e7dea0a77f | |||
| 160489a771 | |||
| 996c6826b9 | |||
| 43b871a124 | |||
| d1bf186040 | |||
| 24c68f100e | |||
| 1bfabf9a83 | |||
| e8a778feca | |||
| 5d4c10c56e | |||
| 60b1c4d8f7 | |||
| f1404cd3ee | |||
| ee4da8a89c | |||
| 5a70a16149 | |||
| f019ba3713 | |||
| 4b966c4845 | |||
| 94703bcf37 | |||
| 40cc6b54c9 | |||
| 584ea7e9f8 | |||
| cbdbc124db | |||
| b9c3fa9401 | |||
| 0e4ec8a8b8 | |||
| c3e4bb80a8 | |||
| 6459840507 | |||
| 87abbe9396 | |||
| d26a8319b6 | |||
| c9c80fd861 | |||
| fea4cc7b3b | |||
| cba5da22ae | |||
| 59a29da054 | |||
| c70674471e | |||
| 7882324439 | |||
| 1f8866a48a | |||
| faf28a6d4e | |||
| 59745e6fb6 | |||
| c8925cd270 | |||
| e35f3b6056 | |||
| d68014ec7b | |||
| b63029054d | |||
| 849c8bee78 | |||
| 70f0384cc3 | |||
| 5459720523 | |||
| a00e2acb5c | |||
| 1d405076e6 | |||
| 7119c566ef | |||
| 0e9428aaae | |||
| fe009ca235 | |||
| a377384553 | |||
| 03c8c323bc | |||
| fdbc380421 | |||
| 7056134b24 | |||
| 93c7552a41 | |||
| 931ed119bb | |||
| 0580842ad2 | |||
| 8d9db83a87 | |||
| c3eb6b2dbf |
41
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
41
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
name: General issue template
|
|
||||||
about: Template for detailed report of issues
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Issue tracker is ONLY used for reporting bugs with technical details. "It doesn't work" or new features should be discussed with our customer support. Please use bug report function in Bridge or contact bridge@protonmail.ch.
|
|
||||||
<!--- Provide a general summary of the issue in the Title above -->
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
<!--- Tell us what should happen -->
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
<!--- Tell us what happens instead of the expected behavior -->
|
|
||||||
|
|
||||||
## Possible Solution
|
|
||||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
|
||||||
|
|
||||||
## Steps to Reproduce
|
|
||||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
|
||||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
4.
|
|
||||||
|
|
||||||
## Version Information
|
|
||||||
<!--- Which version of the app(s) were you using when you experienced this issue? -->
|
|
||||||
|
|
||||||
## Context (Environment)
|
|
||||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
|
||||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
|
||||||
|
|
||||||
## Detailed Description
|
|
||||||
<!--- Provide a detailed description of the change or addition you are proposing -->
|
|
||||||
|
|
||||||
## Possible Implementation
|
|
||||||
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -7,6 +7,7 @@
|
|||||||
*~
|
*~
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.vs
|
||||||
|
|
||||||
# Test files
|
# Test files
|
||||||
godog.test
|
godog.test
|
||||||
@ -35,9 +36,15 @@ cmd/Import-Export/deploy
|
|||||||
proton-bridge
|
proton-bridge
|
||||||
cmd/Desktop-Bridge/*.exe
|
cmd/Desktop-Bridge/*.exe
|
||||||
cmd/launcher/*.exe
|
cmd/launcher/*.exe
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
# Jetbrains (CLion, Golang) cmake build dirs
|
# Jetbrains (CLion, Golang) cmake build dirs
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
|
|
||||||
# Doxygen doc files
|
# Doxygen doc files
|
||||||
_doc/
|
_doc/
|
||||||
|
|
||||||
|
# gRPC auto-generated C++ source files
|
||||||
|
*.pb.cc
|
||||||
|
*.pb.h
|
||||||
|
|||||||
237
.gitlab-ci.yml
237
.gitlab-ci.yml
@ -16,232 +16,35 @@
|
|||||||
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
---
|
---
|
||||||
image: harbor.protontech.ch/docker.io/library/golang:1.18
|
default:
|
||||||
|
tags:
|
||||||
|
- shared-small
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
GOPRIVATE: gitlab.protontech.ch
|
GOPRIVATE: gitlab.protontech.ch
|
||||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- apt update && apt-get -y install libsecret-1-dev
|
- |
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
if [ "$CI_JOB_NAME" != "grype-scan-code-dependencies" ]; then
|
||||||
|
apt update && apt-get -y install libsecret-1-dev
|
||||||
|
git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||||
|
fi
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
- analyse
|
||||||
- test
|
- test
|
||||||
|
- report
|
||||||
- build
|
- build
|
||||||
|
|
||||||
.rules-branch-and-MR-always:
|
include:
|
||||||
rules:
|
- local: ci/setup.yml
|
||||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
- local: ci/rules.yml
|
||||||
when: always
|
- local: ci/env.yml
|
||||||
allow_failure: false
|
- local: ci/test.yml
|
||||||
- when: never
|
- local: ci/report.yml
|
||||||
|
- local: ci/build.yml
|
||||||
|
- component: gitlab.protontech.ch/proton/devops/cicd-components/kits/devsecops/go@~latest
|
||||||
|
inputs:
|
||||||
|
stage: analyse
|
||||||
|
|
||||||
.rules-branch-and-MR-manual:
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: manual
|
|
||||||
allow_failure: true
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
.rules-branch-manual-MR-always:
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: always
|
|
||||||
allow_failure: false
|
|
||||||
- if: $CI_COMMIT_BRANCH
|
|
||||||
when: manual
|
|
||||||
allow_failure: true
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
.rules-branch-manual-MR-always-allow-failure:
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: always
|
|
||||||
allow_failure: true
|
|
||||||
- if: $CI_COMMIT_BRANCH
|
|
||||||
when: manual
|
|
||||||
allow_failure: true
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
|
|
||||||
# Stage: TEST
|
|
||||||
|
|
||||||
lint:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-and-MR-always
|
|
||||||
script:
|
|
||||||
- make lint
|
|
||||||
tags:
|
|
||||||
- medium
|
|
||||||
|
|
||||||
test-linux:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-manual-MR-always
|
|
||||||
script:
|
|
||||||
- make test
|
|
||||||
tags:
|
|
||||||
- medium
|
|
||||||
|
|
||||||
test-linux-race:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-manual-MR-always-allow-failure
|
|
||||||
script:
|
|
||||||
- make test-race
|
|
||||||
tags:
|
|
||||||
- medium
|
|
||||||
|
|
||||||
test-integration:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-manual-MR-always
|
|
||||||
script:
|
|
||||||
- make test-integration
|
|
||||||
tags:
|
|
||||||
- large
|
|
||||||
|
|
||||||
test-integration-race:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-manual-MR-always-allow-failure
|
|
||||||
script:
|
|
||||||
- make test-integration-race
|
|
||||||
tags:
|
|
||||||
- large
|
|
||||||
|
|
||||||
dependency-updates:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- make updates
|
|
||||||
|
|
||||||
# Stage: BUILD
|
|
||||||
|
|
||||||
.build-base:
|
|
||||||
stage: build
|
|
||||||
needs: ["lint"]
|
|
||||||
rules:
|
|
||||||
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
|
|
||||||
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: always
|
|
||||||
allow_failure: false
|
|
||||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: manual
|
|
||||||
allow_failure: true
|
|
||||||
- when: never
|
|
||||||
before_script:
|
|
||||||
- mkdir -p .cache/bin
|
|
||||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
|
||||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
|
||||||
- export PATH=$PATH:$QT6DIR/bin
|
|
||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
|
||||||
script:
|
|
||||||
- make build
|
|
||||||
- git diff && git diff-index --quiet HEAD
|
|
||||||
- make vault-editor
|
|
||||||
artifacts:
|
|
||||||
# Note: The latest artifacts for refs are locked against deletion, and kept
|
|
||||||
# regardless of the expiry time. Introduced in GitLab 13.0 behind a
|
|
||||||
# disabled feature flag, and made the default behavior in GitLab 13.4.
|
|
||||||
expire_in: 1 day
|
|
||||||
when: always
|
|
||||||
paths:
|
|
||||||
- bridge_*.tgz
|
|
||||||
- vault-editor
|
|
||||||
tags:
|
|
||||||
- large
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
extends: .build-base
|
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
|
|
||||||
variables:
|
|
||||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
|
||||||
cache:
|
|
||||||
key: linux-vcpkg
|
|
||||||
paths:
|
|
||||||
- .cache
|
|
||||||
when: 'always'
|
|
||||||
artifacts:
|
|
||||||
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
|
|
||||||
|
|
||||||
build-linux-qa:
|
|
||||||
extends: build-linux
|
|
||||||
variables:
|
|
||||||
BUILD_TAGS: "build_qa"
|
|
||||||
artifacts:
|
|
||||||
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
|
|
||||||
|
|
||||||
|
|
||||||
.build-darwin-base:
|
|
||||||
extends: .build-base
|
|
||||||
before_script:
|
|
||||||
- export PATH=/usr/local/bin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
|
||||||
- export GOPATH=~/go
|
|
||||||
- export PATH=$GOPATH/bin:$PATH
|
|
||||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
|
||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
|
||||||
script:
|
|
||||||
- go version
|
|
||||||
- make build-nogui
|
|
||||||
- git diff && git diff-index --quiet HEAD
|
|
||||||
- make vault-editor
|
|
||||||
cache: {}
|
|
||||||
tags:
|
|
||||||
- macOS
|
|
||||||
|
|
||||||
build-darwin:
|
|
||||||
extends: .build-darwin-base
|
|
||||||
artifacts:
|
|
||||||
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
|
|
||||||
|
|
||||||
build-darwin-qa:
|
|
||||||
extends: .build-darwin-base
|
|
||||||
variables:
|
|
||||||
BUILD_TAGS: "build_qa"
|
|
||||||
artifacts:
|
|
||||||
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
|
|
||||||
|
|
||||||
|
|
||||||
.build-windows-base:
|
|
||||||
extends: .build-base
|
|
||||||
before_script:
|
|
||||||
- export GOROOT=/c/Go1.18/
|
|
||||||
- export PATH=$GOROOT/bin:$PATH
|
|
||||||
- export GOARCH=amd64
|
|
||||||
- export GOPATH=~/go18
|
|
||||||
- export GO111MODULE=on
|
|
||||||
- export PATH="${GOPATH}/bin:${PATH}"
|
|
||||||
- export MSYSTEM=
|
|
||||||
- export QT6DIR=/c/grrrQt/6.3.1/msvc2019_64
|
|
||||||
- export PATH=$PATH:${QT6DIR}/bin
|
|
||||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
|
||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
|
||||||
script:
|
|
||||||
- make build-nogui
|
|
||||||
- git diff && git diff-index --quiet HEAD
|
|
||||||
- make vault-editor
|
|
||||||
tags:
|
|
||||||
- windows-bridge
|
|
||||||
|
|
||||||
build-windows:
|
|
||||||
extends: .build-windows-base
|
|
||||||
artifacts:
|
|
||||||
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
|
||||||
|
|
||||||
build-windows-qa:
|
|
||||||
extends: .build-windows-base
|
|
||||||
variables:
|
|
||||||
BUILD_TAGS: "build_qa"
|
|
||||||
artifacts:
|
|
||||||
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
|
||||||
|
|
||||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
|
||||||
|
|||||||
1
.gitlab/CODEOWNERS
Normal file
1
.gitlab/CODEOWNERS
Normal file
@ -0,0 +1 @@
|
|||||||
|
* @go/bridge-ppl/devs
|
||||||
@ -2,11 +2,12 @@
|
|||||||
run:
|
run:
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
skip-dirs:
|
skip-dirs:
|
||||||
- pkg/mime
|
|
||||||
- extern
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
|
exclude-dirs:
|
||||||
|
- pkg/mime
|
||||||
|
- extern
|
||||||
exclude:
|
exclude:
|
||||||
- Using the variable on range scope `tt` in function literal
|
- Using the variable on range scope `tt` in function literal
|
||||||
# For now we are missing a lot of comments.
|
# For now we are missing a lot of comments.
|
||||||
@ -23,7 +24,6 @@ issues:
|
|||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- dupl
|
- dupl
|
||||||
- funlen
|
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gosec
|
- gosec
|
||||||
@ -32,7 +32,14 @@ issues:
|
|||||||
- path: test
|
- path: test
|
||||||
linters:
|
linters:
|
||||||
- dupl
|
- dupl
|
||||||
- funlen
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
- gosec
|
||||||
|
- goconst
|
||||||
|
- dogsled
|
||||||
|
- path: utils/smtp-send
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gosec
|
- gosec
|
||||||
@ -50,21 +57,17 @@ linters:
|
|||||||
disable-all: true
|
disable-all: true
|
||||||
|
|
||||||
enable:
|
enable:
|
||||||
- deadcode # Finds unused code [fast: true, auto-fix: false]
|
|
||||||
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
|
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
|
||||||
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
|
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
|
||||||
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
|
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
|
||||||
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
||||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
|
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
|
||||||
- structcheck # Finds unused struct fields [fast: true, auto-fix: false]
|
|
||||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
||||||
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
||||||
- varcheck # Finds unused global variables and constants [fast: true, auto-fix: false]
|
|
||||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
|
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
|
||||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
#- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
||||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
||||||
- dupl # Tool for code clone detection [fast: true, auto-fix: false]
|
- dupl # Tool for code clone detection [fast: true, auto-fix: false]
|
||||||
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
|
||||||
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
|
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
|
||||||
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
|
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
|
||||||
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
||||||
@ -84,7 +87,7 @@ linters:
|
|||||||
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
|
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
|
||||||
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
|
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
|
||||||
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
|
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
|
||||||
- exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
|
- copyloopvar # detects places where loop variables are copied.
|
||||||
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
|
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
|
||||||
- godot # Check if comments end in a period [fast: true, auto-fix: true]
|
- godot # Check if comments end in a period [fast: true, auto-fix: true]
|
||||||
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]
|
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]
|
||||||
@ -122,3 +125,8 @@ linters:
|
|||||||
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
|
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
|
||||||
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
|
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
|
||||||
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
||||||
|
|
||||||
|
# Deprecated:
|
||||||
|
# - structcheck # Finds unused struct fields [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||||
|
# - deadcode # Finds unused code [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||||
|
# - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||||
|
|||||||
2
.grype.yaml
Normal file
2
.grype.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Check out for configuration details: https://github.com/anchore/grype?tab=readme-ov-file#configuration
|
||||||
|
fail-on-severity: "medium"
|
||||||
24
BUILDS.md
24
BUILDS.md
@ -3,20 +3,23 @@
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
* 64-bit OS:
|
* 64-bit OS:
|
||||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||||
* Go 1.18
|
* Go 1.23.4
|
||||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
||||||
- For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||||
* GCC (linux), msvc (windows) or Xcode (macOS)
|
* GCC (Linux), msvc (Windows) or Xcode (macOS)
|
||||||
* Windres (windows)
|
* Windres (Windows)
|
||||||
* libglvnd and libsecret development files (linux)
|
* libglvnd and libsecret development files (Linux)
|
||||||
|
* pkg-config (Linux)
|
||||||
|
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
|
||||||
|
the Mesa OpenGL development files are also needed.
|
||||||
|
|
||||||
To enable the sending of crash reports using Sentry please set the
|
To enable the sending of crash reports using Sentry please set the
|
||||||
`main.DSNSentry` value with the client key of your sentry project before build.
|
`DSN_SENTRY` environment variable with the client key of your sentry project before build.
|
||||||
Otherwise, the sending of crash reports will be disabled.
|
Otherwise, the sending of crash reports will be disabled.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
In order to build Bridge app with Qt interface we are using
|
In order to build Bridge app with Qt interface we are using
|
||||||
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||||
|
|
||||||
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
||||||
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
||||||
@ -44,9 +47,10 @@ make build
|
|||||||
make build-nogui
|
make build-nogui
|
||||||
```
|
```
|
||||||
|
|
||||||
* Bridge without GUI will start by default without any interface (i.e., there is no way to add or remove client, get bridge password, etc)
|
* To launch Bridge without GUI, you can invoke the `bridge` executable with one the following command-line switches:
|
||||||
* Bridge always has the option (whether built with Qt or without) to use a CLI interface by starting it with the argument `-c`
|
* `--noninteractive` or `-n` to start Bridge without any interface (i.e., there is no way to add or remove client, get bridge password, etc.)
|
||||||
* NOTE: You still need to setup supported keychain on your system
|
* `--cli` or `-c` to start Bridge with an interactive terminal interface.
|
||||||
|
* NOTE: You still need to set up a supported keychain on your system.
|
||||||
|
|
||||||
## Launchers
|
## Launchers
|
||||||
Launchers are only included in official distributions and provide the public
|
Launchers are only included in official distributions and provide the public
|
||||||
|
|||||||
@ -1,10 +1,78 @@
|
|||||||
# Contribution Policy
|
# Contributing guidelines
|
||||||
|
|
||||||
|
The following document describes how to contribute to the project. In this context, contribution does not only mean code contribution but also reporting issues, requesting new features, or just asking for help.
|
||||||
|
|
||||||
|
## Reporting issues
|
||||||
|
|
||||||
|
In case you experience issues while using the application, our request is to contact Proton customer support directly.
|
||||||
|
|
||||||
|
The benefits of using Proton customer support are
|
||||||
|
|
||||||
|
- Available 24/7/365.
|
||||||
|
- Provides priority support based on subscription type.
|
||||||
|
- Will escalate the issue to the developers every time it becomes too technical or they do not know the answer to a question.
|
||||||
|
- Easier to detect systematic issues by connecting similar reports.
|
||||||
|
- Possible to quickly derive frequency of an issue.
|
||||||
|
- Can assist you to transfer sensitive information safely to us.
|
||||||
|
|
||||||
|
To speed up the communication with customer support, consider the following:
|
||||||
|
|
||||||
|
- Whenever is possible, use the in-app bug report feature. It provides an application specific guide compared to using the generic report form on web.
|
||||||
|
- Whenever is possible, proactively attach logs to your report. Reporting an issue from the application can help you in that.
|
||||||
|
- Check whether your system is officially supported by Proton, including the source of the installer. We cannot provide help when the application is packaged by a third party or when the application is used on systems that we do not prepare to support.
|
||||||
|
- If your report is a feature request, see the Feature request section. In case it is an issue related to application security, see the Security vulnerabilities section.
|
||||||
|
|
||||||
|
In the past, we used GitHub issue tracker for more technical issues in parallel to Proton customer support, but we run into limitations with this approach:
|
||||||
|
|
||||||
|
- Monitoring GitHub issue tracker took development time as it was managed by the development team.
|
||||||
|
- It made issue frequency tracking challenging because we did not have a single point of entry for issues.
|
||||||
|
- Users were confused what technical issue means, and used the GitHub issue tracker for feature requests, or non-technical discussions.
|
||||||
|
- Users sometimes shared sensitive data through the GitHub issue tracker.
|
||||||
|
|
||||||
|
For the above reasons, we do not use GitHub issue tracker anymore but ask you to contact our customer support in case you run into a problem.
|
||||||
|
|
||||||
|
### Security vulnerabilities
|
||||||
|
|
||||||
|
Proton runs a bug bounty program for security vulnerabilities. They differ from normal bug reports in the following ways:
|
||||||
|
|
||||||
|
- These reports go directly to our security team.
|
||||||
|
- They expect deeper explanation of the issue.
|
||||||
|
- Depending on the finding, they may be financially rewarded.
|
||||||
|
|
||||||
|
More information about the program can be found [here](https://proton.me/security/bug-bounty).
|
||||||
|
|
||||||
|
## Feature requests
|
||||||
|
|
||||||
|
What someone considers as a bug is sometimes a feature, and sometimes, a missing feature is considered as a bug. Instead of reporting feature requests as bugs, we setup a UserVoice page to allow our users to share their preferences. UserVoice also makes it possible to vote on other feature requests, making the community preference public.
|
||||||
|
|
||||||
|
Our product team frequently monitors UserVoice, and the features listed there are taken into account in our planning.
|
||||||
|
|
||||||
|
Examples for UserVoice requests:
|
||||||
|
|
||||||
|
- Extending the officially supported environments (e.g., operating systems, clients, or computer architectures).
|
||||||
|
- Requesting new features.
|
||||||
|
- Integration with non-Proton services.
|
||||||
|
|
||||||
|
UserVoice is available [here](https://protonmail.uservoice.com/).
|
||||||
|
|
||||||
|
## Asking for help
|
||||||
|
|
||||||
|
The best ways to get answer for generic questions or to get help with setting up the system is to interact with our active community on [Reddit](https://reddit.com/r/ProtonMail/) or to contact customer support.
|
||||||
|
|
||||||
|
## Code contribution
|
||||||
|
|
||||||
|
We are grateful if you can contribute directly with code. In that case there is nothing else to do than to open a pull request.
|
||||||
|
|
||||||
|
The following is worthwhile noting
|
||||||
|
|
||||||
|
- The project is primarily developed on an internal repository, and the one on GitHub is only a mirror of it. For that reason, the merge request will not be merged on GitHub but added to the project internally. We are keeping the original author in the change set to respect the contribution.
|
||||||
|
- The application is used on numerous platforms and by many third party clients. To have higher chance your change to be accepted, consider all supported dependencies.
|
||||||
|
- Give detailed description of the issue, preferably with test steps to reproduce the original issue, and to verify the fix. It is even better if you also extend the automated tests.
|
||||||
|
|
||||||
|
### Contribution policy
|
||||||
|
|
||||||
By making a contribution to this project:
|
By making a contribution to this project:
|
||||||
|
|
||||||
1. I assign any and all copyright related to the contribution to Proton AG;
|
1. You assign any and all copyright related to the contribution to Proton AG;
|
||||||
2. I certify that the contribution was created in whole by me;
|
2. You certify that the contribution was created in whole by you;
|
||||||
3. I understand and agree that this project and the contribution are public
|
3. You understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information you submit with it) is maintained indefinitely and may be redistributed with this project or the open source license(s) involved.
|
||||||
and that a record of the contribution (including all personal information I
|
|
||||||
submit with it) is maintained indefinitely and may be redistributed with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
|
|||||||
@ -26,7 +26,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [gluon](https://github.com/ProtonMail/gluon) available under [license](https://github.com/ProtonMail/gluon/blob/master/LICENSE)
|
* [gluon](https://github.com/ProtonMail/gluon) available under [license](https://github.com/ProtonMail/gluon/blob/master/LICENSE)
|
||||||
* [go-autostart](https://github.com/ProtonMail/go-autostart) available under [license](https://github.com/ProtonMail/go-autostart/blob/master/LICENSE)
|
* [go-autostart](https://github.com/ProtonMail/go-autostart) available under [license](https://github.com/ProtonMail/go-autostart/blob/master/LICENSE)
|
||||||
* [go-proton-api](https://github.com/ProtonMail/go-proton-api) available under [license](https://github.com/ProtonMail/go-proton-api/blob/master/LICENSE)
|
* [go-proton-api](https://github.com/ProtonMail/go-proton-api) available under [license](https://github.com/ProtonMail/go-proton-api/blob/master/LICENSE)
|
||||||
* [go-rfc5322](https://github.com/ProtonMail/go-rfc5322) available under [license](https://github.com/ProtonMail/go-rfc5322/blob/master/LICENSE)
|
|
||||||
* [gopenpgp](https://github.com/ProtonMail/gopenpgp/v2) available under [license](https://github.com/ProtonMail/gopenpgp/v2/blob/master/LICENSE)
|
* [gopenpgp](https://github.com/ProtonMail/gopenpgp/v2) available under [license](https://github.com/ProtonMail/gopenpgp/v2/blob/master/LICENSE)
|
||||||
* [goquery](https://github.com/PuerkitoBio/goquery) available under [license](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
|
* [goquery](https://github.com/PuerkitoBio/goquery) available under [license](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
|
||||||
* [ishell](https://github.com/abiosoft/ishell) available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
* [ishell](https://github.com/abiosoft/ishell) available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
||||||
@ -41,43 +40,46 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
||||||
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||||
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||||
|
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||||
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
||||||
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
||||||
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
||||||
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
|
|
||||||
* [dbus](https://github.com/godbus/dbus) available under [license](https://github.com/godbus/dbus/blob/master/LICENSE)
|
* [dbus](https://github.com/godbus/dbus) available under [license](https://github.com/godbus/dbus/blob/master/LICENSE)
|
||||||
* [mock](https://github.com/golang/mock) available under [license](https://github.com/golang/mock/blob/master/LICENSE)
|
* [mock](https://github.com/golang/mock) available under [license](https://github.com/golang/mock/blob/master/LICENSE)
|
||||||
* [go-cmp](https://github.com/google/go-cmp) available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
|
* [go-cmp](https://github.com/google/go-cmp) available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
|
||||||
* [uuid](https://github.com/google/uuid) available under [license](https://github.com/google/uuid/blob/master/LICENSE)
|
* [uuid](https://github.com/google/uuid) available under [license](https://github.com/google/uuid/blob/master/LICENSE)
|
||||||
* [go-multierror](https://github.com/hashicorp/go-multierror) available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
|
* [go-multierror](https://github.com/hashicorp/go-multierror) available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
|
||||||
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
||||||
|
* [go-locale](https://github.com/jeandeaual/go-locale) available under [license](https://github.com/jeandeaual/go-locale/blob/master/LICENSE)
|
||||||
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
||||||
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
||||||
|
* [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
|
||||||
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
|
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
|
||||||
* [profile](https://github.com/pkg/profile) available under [license](https://github.com/pkg/profile/blob/master/LICENSE)
|
* [profile](https://github.com/pkg/profile) available under [license](https://github.com/pkg/profile/blob/master/LICENSE)
|
||||||
* [logrus](https://github.com/sirupsen/logrus) available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
* [logrus](https://github.com/sirupsen/logrus) available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
||||||
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||||
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
|
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
|
||||||
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
|
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
|
||||||
* [goleak](https://go.uber.org/goleak)
|
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
|
||||||
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
||||||
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
||||||
|
* [oauth2](https://golang.org/x/oauth2) available under [license](https://cs.opensource.google/go/x/oauth2/+/master:LICENSE)
|
||||||
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||||
* [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE)
|
* [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE)
|
||||||
|
* [api](https://google.golang.org/api) available under [license](https://pkg.go.dev/google.golang.org/api?tab=licenses)
|
||||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
||||||
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
||||||
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
||||||
* [atlas](https://ariga.io/atlas)
|
* [compute](https://cloud.google.com/go/compute) available under [license](https://pkg.go.dev/cloud.google.com/go/compute?tab=licenses)
|
||||||
* [ent](https://entgo.io/ent)
|
* [metadata](https://cloud.google.com/go/compute/metadata) available under [license](https://pkg.go.dev/cloud.google.com/go/compute/metadata?tab=licenses)
|
||||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||||
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
||||||
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
|
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
|
||||||
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||||
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
|
|
||||||
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||||
* [antlr](https://github.com/antlr/antlr4/runtime/Go/antlr) available under [license](https://github.com/antlr/antlr4/runtime/Go/antlr/blob/master/LICENSE)
|
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
|
||||||
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
|
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
|
||||||
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||||
* [circl](https://github.com/cloudflare/circl) available under [license](https://github.com/cloudflare/circl/blob/master/LICENSE)
|
* [circl](https://github.com/cloudflare/circl) available under [license](https://github.com/cloudflare/circl/blob/master/LICENSE)
|
||||||
* [go-md2man](https://github.com/cpuguy83/go-md2man/v2) available under [license](https://github.com/cpuguy83/go-md2man/v2/blob/master/LICENSE)
|
* [go-md2man](https://github.com/cpuguy83/go-md2man/v2) available under [license](https://github.com/cpuguy83/go-md2man/v2/blob/master/LICENSE)
|
||||||
@ -87,51 +89,60 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
||||||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
||||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||||
|
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
||||||
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
|
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
|
||||||
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
|
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
|
||||||
* [inflect](https://github.com/go-openapi/inflect) available under [license](https://github.com/go-openapi/inflect/blob/master/LICENSE)
|
|
||||||
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
|
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
|
||||||
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
|
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
|
||||||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||||
|
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
|
||||||
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||||
|
* [groupcache](https://github.com/golang/groupcache) available under [license](https://github.com/golang/groupcache/blob/master/LICENSE)
|
||||||
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
|
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
|
||||||
|
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
|
||||||
|
* [enterprise-certificate-proxy](https://github.com/googleapis/enterprise-certificate-proxy) available under [license](https://github.com/googleapis/enterprise-certificate-proxy/blob/master/LICENSE)
|
||||||
|
* [gax-go](https://github.com/googleapis/gax-go/v2) available under [license](https://github.com/googleapis/gax-go/v2/blob/master/LICENSE)
|
||||||
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
|
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
|
||||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||||
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
|
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
|
||||||
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
|
|
||||||
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
|
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
|
||||||
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
|
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
|
||||||
|
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
|
||||||
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
|
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
|
||||||
* [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE)
|
* [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE)
|
||||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
||||||
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
|
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
|
||||||
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
|
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
|
||||||
* [go-wordwrap](https://github.com/mitchellh/go-wordwrap) available under [license](https://github.com/mitchellh/go-wordwrap/blob/master/LICENSE)
|
|
||||||
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
|
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
|
||||||
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
|
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
|
||||||
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||||
* [go-toml](https://github.com/pelletier/go-toml/v2) available under [license](https://github.com/pelletier/go-toml/v2/blob/master/LICENSE)
|
* [go-toml](https://github.com/pelletier/go-toml/v2) available under [license](https://github.com/pelletier/go-toml/v2/blob/master/LICENSE)
|
||||||
|
* [lz4](https://github.com/pierrec/lz4/v4) available under [license](https://github.com/pierrec/lz4/v4/blob/master/LICENSE)
|
||||||
* [go-difflib](https://github.com/pmezard/go-difflib) available under [license](https://github.com/pmezard/go-difflib/blob/master/LICENSE)
|
* [go-difflib](https://github.com/pmezard/go-difflib) available under [license](https://github.com/pmezard/go-difflib/blob/master/LICENSE)
|
||||||
* [procfs](https://github.com/prometheus/procfs) available under [license](https://github.com/prometheus/procfs/blob/master/LICENSE)
|
* [procfs](https://github.com/prometheus/procfs) available under [license](https://github.com/prometheus/procfs/blob/master/LICENSE)
|
||||||
* [uniseg](https://github.com/rivo/uniseg) available under [license](https://github.com/rivo/uniseg/blob/master/LICENSE)
|
* [uniseg](https://github.com/rivo/uniseg) available under [license](https://github.com/rivo/uniseg/blob/master/LICENSE)
|
||||||
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
|
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
|
||||||
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
|
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
|
||||||
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
||||||
|
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
|
||||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||||
* [go-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/master/LICENSE)
|
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json) available under [license](https://gitlab.com/c0b/go-ordered-json/blob/master/LICENSE)
|
||||||
|
* [go.opencensus.io](https://pkg.go.dev/go.opencensus.io?tab=licenses) available under [license](https://pkg.go.dev/go.opencensus.io?tab=licenses)
|
||||||
|
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||||
* [genproto](https://google.golang.org/genproto)
|
* [appengine](https://google.golang.org/appengine) available under [license](https://pkg.go.dev/google.golang.org/appengine?tab=licenses)
|
||||||
gopkg.in/yaml.v2
|
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
|
||||||
gopkg.in/yaml.v3
|
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
|
||||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||||
|
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
|
||||||
|
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
|
||||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||||
<!-- END AUTOGEN -->
|
<!-- END AUTOGEN -->
|
||||||
|
|||||||
992
Changelog.md
992
Changelog.md
File diff suppressed because it is too large
Load Diff
135
Makefile
135
Makefile
@ -1,17 +1,18 @@
|
|||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
|
||||||
# By default, the target OS is the same as the host OS,
|
# By default, the target OS is the same as the host OS,
|
||||||
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
|
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
|
||||||
GOOS:=$(shell go env GOOS)
|
GOOS:=$(shell go env GOOS)
|
||||||
TARGET_CMD?=Desktop-Bridge
|
TARGET_CMD?=Desktop-Bridge
|
||||||
TARGET_OS?=${GOOS}
|
TARGET_OS?=${GOOS}
|
||||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
ROOT_DIR:=$(realpath .)
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.0.10+git
|
BRIDGE_APP_VERSION?=3.18.0+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -19,20 +20,26 @@ SRC_ICO:=bridge.ico
|
|||||||
SRC_ICNS:=Bridge.icns
|
SRC_ICNS:=Bridge.icns
|
||||||
SRC_SVG:=bridge.svg
|
SRC_SVG:=bridge.svg
|
||||||
EXE_NAME:=proton-bridge
|
EXE_NAME:=proton-bridge
|
||||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
REVISION:=$(shell "${ROOT_DIR}/utils/get_revision.sh" rev)
|
||||||
|
TAG:=$(shell "${ROOT_DIR}/utils/get_revision.sh" tag)
|
||||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||||
MACOS_MIN_VERSION_ARM64=11.0
|
MACOS_MIN_VERSION_ARM64=11.0
|
||||||
MACOS_MIN_VERSION_AMD64=10.15
|
MACOS_MIN_VERSION_AMD64=10.15
|
||||||
|
BUILD_ENV?=dev
|
||||||
|
|
||||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||||
BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt'
|
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} Tag=${TAG} BuildTime=${BUILD_TIME})
|
||||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
|
||||||
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
||||||
|
|
||||||
ifneq "${BUILD_LDFLAGS}" ""
|
ifneq "${DSN_SENTRY}" ""
|
||||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.DSNSentry=${DSN_SENTRY}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifneq "${BUILD_ENV}" ""
|
||||||
|
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.BuildEnv=${BUILD_ENV}
|
||||||
|
endif
|
||||||
|
|
||||||
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
||||||
ifeq "${TARGET_OS}" "windows"
|
ifeq "${TARGET_OS}" "windows"
|
||||||
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
|
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
|
||||||
@ -40,7 +47,6 @@ ifeq "${TARGET_OS}" "windows"
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
||||||
BUILD_FLAGS_GUI+=-ldflags "${GO_LDFLAGS}"
|
|
||||||
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
||||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||||
DIRNAME:=$(shell basename ${CURDIR})
|
DIRNAME:=$(shell basename ${CURDIR})
|
||||||
@ -96,9 +102,9 @@ endif
|
|||||||
|
|
||||||
ifeq "${GOOS}" "windows"
|
ifeq "${GOOS}" "windows"
|
||||||
go-build-finalize= \
|
go-build-finalize= \
|
||||||
powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} && \
|
$(if $(4),cp "${ROOT_DIR}/${RESOURCE_FILE}" ${4} &&,) \
|
||||||
$(call go-build,$(1),$(2),$(3)) && \
|
$(call go-build,$(1),$(2),$(3)) \
|
||||||
powershell Remove-Item ${4} -Force
|
$(if $(4), && rm -f ${4},)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
${EXE_NAME}: gofiles ${RESOURCE_FILE}
|
${EXE_NAME}: gofiles ${RESOURCE_FILE}
|
||||||
@ -112,7 +118,10 @@ versioner:
|
|||||||
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
||||||
|
|
||||||
vault-editor:
|
vault-editor:
|
||||||
go build -tags debug -o vault-editor utils/vault-editor/main.go
|
$(call go-build-finalize,-tags=debug,"vault-editor","./utils/vault-editor/main.go")
|
||||||
|
|
||||||
|
bridge-rollout:
|
||||||
|
$(call go-build-finalize,, "bridge-rollout","./utils/bridge-rollout/bridge-rollout.go")
|
||||||
|
|
||||||
hasher:
|
hasher:
|
||||||
go build -o hasher utils/hasher/main.go
|
go build -o hasher utils/hasher/main.go
|
||||||
@ -154,9 +163,12 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
|
|||||||
BRIDGE_VENDOR="${APP_VENDOR}" \
|
BRIDGE_VENDOR="${APP_VENDOR}" \
|
||||||
BRIDGE_APP_VERSION=${APP_VERSION} \
|
BRIDGE_APP_VERSION=${APP_VERSION} \
|
||||||
BRIDGE_REVISION=${REVISION} \
|
BRIDGE_REVISION=${REVISION} \
|
||||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
BRIDGE_TAG=${TAG} \
|
||||||
|
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
|
||||||
|
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||||
BRIDGE_GUI_BUILD_CONFIG=Release \
|
BRIDGE_GUI_BUILD_CONFIG=Release \
|
||||||
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
|
BRIDGE_BUILD_ENV=${BUILD_ENV} \
|
||||||
|
BRIDGE_INSTALL_PATH="${ROOT_DIR}/${DEPLOY_DIR}/${GOOS}" \
|
||||||
./build.sh install
|
./build.sh install
|
||||||
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
||||||
|
|
||||||
@ -177,7 +189,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
|
|||||||
|
|
||||||
## Dev dependencies
|
## Dev dependencies
|
||||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
||||||
LINTVER:="v1.50.0"
|
LINTVER:="v1.61.0"
|
||||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||||
|
|
||||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||||
@ -221,14 +233,28 @@ add-license:
|
|||||||
change-copyright-year:
|
change-copyright-year:
|
||||||
./utils/missing_license.sh change-year
|
./utils/missing_license.sh change-year
|
||||||
|
|
||||||
|
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
|
||||||
|
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
|
||||||
|
|
||||||
test: gofiles
|
test: gofiles
|
||||||
go test -v -timeout=5m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
mkdir -p coverage/unit-${GOOS}
|
||||||
|
go test \
|
||||||
|
-v -timeout=20m -p=1 -count=1 \
|
||||||
|
${GOCOVERAGE} \
|
||||||
|
-run=${TESTRUN} ./internal/... ./pkg/... \
|
||||||
|
${GOCOVERDIR}/unit-${GOOS}
|
||||||
|
|
||||||
test-race: gofiles
|
test-race: gofiles
|
||||||
go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||||
|
|
||||||
test-integration: gofiles
|
test-integration: gofiles
|
||||||
go test -v -timeout=10m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
mkdir -p coverage/integration
|
||||||
|
go test \
|
||||||
|
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
|
||||||
|
${GOCOVERAGE} \
|
||||||
|
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||||
|
${GOCOVERDIR}/integration
|
||||||
|
|
||||||
|
|
||||||
test-integration-debug: gofiles
|
test-integration-debug: gofiles
|
||||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||||
@ -236,6 +262,23 @@ test-integration-debug: gofiles
|
|||||||
test-integration-race: gofiles
|
test-integration-race: gofiles
|
||||||
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
|
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
|
||||||
|
|
||||||
|
test-integration-nightly: gofiles
|
||||||
|
mkdir -p coverage/integration
|
||||||
|
gotestsum \
|
||||||
|
--junitfile tests/result/feature-tests.xml -- \
|
||||||
|
-v -timeout=90m -p=1 -count=1 -tags=test_integration \
|
||||||
|
${GOCOVERAGE} \
|
||||||
|
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||||
|
${GOCOVERDIR}/integration \
|
||||||
|
nightly
|
||||||
|
|
||||||
|
fuzz: gofiles
|
||||||
|
go test -fuzz=FuzzUnmarshal -parallel=4 -fuzztime=60s $(PWD)/internal/legacy/credentials
|
||||||
|
go test -fuzz=FuzzNewParser -parallel=4 -fuzztime=60s $(PWD)/pkg/message/parser
|
||||||
|
go test -fuzz=FuzzReadHeaderBody -parallel=4 -fuzztime=60s $(PWD)/pkg/message
|
||||||
|
go test -fuzz=FuzzDecodeHeader -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
|
||||||
|
go test -fuzz=FuzzDecodeCharset -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
|
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
|
||||||
go tool pprof -png -output bench_mem.png bench_mem.pprof
|
go tool pprof -png -output bench_mem.png bench_mem.pprof
|
||||||
@ -247,11 +290,28 @@ coverage: test
|
|||||||
mocks:
|
mocks:
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/bridge TLSReporter,ProxyController,Autostarter > tmp
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/bridge TLSReporter,ProxyController,Autostarter > tmp
|
||||||
mv tmp internal/bridge/mocks/mocks.go
|
mv tmp internal/bridge/mocks/mocks.go
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||||
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
|
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
|
||||||
|
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
|
||||||
|
EventSource,EventIDStore > internal/services/userevents/mocks/mocks.go
|
||||||
|
mockgen --package userevents github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
|
||||||
|
EventSubscriber,MessageEventHandler,LabelEventHandler,AddressEventHandler,RefreshEventHandler,UserEventHandler,UserUsedSpaceEventHandler > tmp
|
||||||
|
mv tmp internal/services/userevents/mocks_test.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/events EventPublisher \
|
||||||
|
> internal/events/mocks/mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity IdentityProvider,Telemetry \
|
||||||
|
> internal/services/useridentity/mocks/mocks.go
|
||||||
|
mockgen --self_package "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice" -package syncservice github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice \
|
||||||
|
ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStageOutput,MetadataStageInput,MetadataStageOutput,\
|
||||||
|
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
|
||||||
|
> tmp
|
||||||
|
mv tmp internal/services/syncservice/mocks_test.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/gluon/connector IMAPStateWrite > internal/services/imapservice/mocks/mocks.go
|
||||||
|
|
||||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report
|
||||||
|
|
||||||
lint-license:
|
lint-license:
|
||||||
./utils/missing_license.sh check
|
./utils/missing_license.sh check
|
||||||
@ -267,12 +327,11 @@ lint-golang:
|
|||||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
gobinsec: gobinsec-cache.yml build
|
lint-bug-report:
|
||||||
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json"
|
||||||
|
|
||||||
gobinsec-cache.yml:
|
lint-bug-report-preview:
|
||||||
./utils/gobinsec_update.sh
|
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
|
||||||
cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
|
|
||||||
|
|
||||||
updates: install-go-mod-outdated
|
updates: install-go-mod-outdated
|
||||||
# Uncomment the "-ci" to fail the job if something can be updated.
|
# Uncomment the "-ci" to fail the job if something can be updated.
|
||||||
@ -294,7 +353,7 @@ gofiles: ./internal/bridge/credits.go
|
|||||||
cd ./utils/ && ./credits.sh bridge
|
cd ./utils/ && ./credits.sh bridge
|
||||||
|
|
||||||
## Run and debug
|
## Run and debug
|
||||||
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-qml-preview clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-gui-tester clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
||||||
|
|
||||||
LOG?=debug
|
LOG?=debug
|
||||||
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
||||||
@ -319,14 +378,32 @@ run-nogui: build-nogui clean-vendor gofiles
|
|||||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
||||||
|
|
||||||
run-debug:
|
run-debug:
|
||||||
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
dlv debug \
|
||||||
|
--build-flags "-ldflags '-X github.com/ProtonMail/proton-bridge/v3/internal/constants.Version=3.1.0+git'" \
|
||||||
|
./cmd/Desktop-Bridge/main.go \
|
||||||
|
-- \
|
||||||
|
-n -l=trace
|
||||||
|
|
||||||
|
ifeq "${TARGET_OS}" "windows"
|
||||||
|
EXE_SUFFIX=.exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
bridge-gui-tester: build-gui
|
||||||
|
cp ./cmd/Desktop-Bridge/deploy/${TARGET_OS}/bridge-gui${EXE_SUFFIX} .
|
||||||
|
cd ./internal/frontend/bridge-gui/bridge-gui-tester && cmake . && make
|
||||||
|
|
||||||
|
run-gui-tester: bridge-gui-tester
|
||||||
|
# copying tester as bridge so bridge-gui will start it and connect to it automatically
|
||||||
|
cp ./internal/frontend/bridge-gui/bridge-gui-tester/bridge-gui-tester${EXE_SUFFIX} bridge${EXE_SUFFIX}
|
||||||
|
./bridge-gui${EXE_SUFFIX}
|
||||||
|
|
||||||
|
|
||||||
clean-vendor:
|
clean-vendor:
|
||||||
rm -rf ./vendor
|
rm -rf ./vendor
|
||||||
|
|
||||||
clean-gui:
|
clean-gui:
|
||||||
cd internal/frontend/bridge-gui/ && \
|
cd internal/frontend/bridge-gui/ && \
|
||||||
rm -f Version.h && \
|
rm -f BuildConfig.h && \
|
||||||
rm -rf cmake-build-*/
|
rm -rf cmake-build-*/
|
||||||
|
|
||||||
clean-vcpkg:
|
clean-vcpkg:
|
||||||
@ -349,6 +426,6 @@ clean: clean-vendor clean-gui clean-vcpkg
|
|||||||
.PHONY: generate
|
.PHONY: generate
|
||||||
generate:
|
generate:
|
||||||
go generate ./...
|
go generate ./...
|
||||||
$(MAKE) add-license
|
$(MAKE) build
|
||||||
|
|
||||||
.FORCE:
|
.FORCE:
|
||||||
|
|||||||
72
README.md
72
README.md
@ -1,7 +1,7 @@
|
|||||||
# Proton Mail Bridge and Import Export app
|
# Proton Mail Bridge
|
||||||
Copyright (c) 2022 Proton AG
|
Copyright (c) 2025 Proton AG
|
||||||
|
|
||||||
This repository holds the Proton Mail Bridge and the Proton Mail Import-Export applications.
|
This repository holds the Proton Mail Bridge application.
|
||||||
For a detailed build information see [BUILDS](./BUILDS.md).
|
For a detailed build information see [BUILDS](./BUILDS.md).
|
||||||
The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md).
|
The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md).
|
||||||
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
|
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||||
@ -13,7 +13,7 @@ Proton Mail Bridge for e-mail clients.
|
|||||||
When launched, Bridge will initialize local IMAP/SMTP servers and render
|
When launched, Bridge will initialize local IMAP/SMTP servers and render
|
||||||
its GUI.
|
its GUI.
|
||||||
|
|
||||||
To configure an e-mail client, firstly log in using your Proton Mail credentials.
|
To configure an e-mail client, first log in using your Proton Mail credentials.
|
||||||
Open your e-mail client and add a new account using the settings which are
|
Open your e-mail client and add a new account using the settings which are
|
||||||
located in the Bridge GUI. The client will only be able to sync with
|
located in the Bridge GUI. The client will only be able to sync with
|
||||||
your Proton Mail account when the Bridge is running, thus the option
|
your Proton Mail account when the Bridge is running, thus the option
|
||||||
@ -24,10 +24,10 @@ background.
|
|||||||
|
|
||||||
More details [on the public website](https://proton.me/mail/bridge).
|
More details [on the public website](https://proton.me/mail/bridge).
|
||||||
|
|
||||||
## Launchers
|
## Launcher
|
||||||
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.
|
The launcher is a binary used to run the Proton Mail Bridge.
|
||||||
|
|
||||||
Official distributions of the Proton Mail Bridge and Import-Export apps contain
|
The Official distribution of the Proton Mail Bridge application contains
|
||||||
both a launcher and the app itself. The launcher is installed in a protected
|
both a launcher and the app itself. The launcher is installed in a protected
|
||||||
area of the system (i.e. an area accessible only with admin privileges) and is
|
area of the system (i.e. an area accessible only with admin privileges) and is
|
||||||
used to run the app. The launcher ensures that nobody tampered with the app's
|
used to run the app. The launcher ensures that nobody tampered with the app's
|
||||||
@ -37,7 +37,7 @@ feature enables the app to securely update itself automatically without asking
|
|||||||
the user for a password.
|
the user for a password.
|
||||||
|
|
||||||
## Keychain
|
## Keychain
|
||||||
You need to have a keychain in order to run the Proton Mail Bridge. On Mac or
|
You need to have a keychain in order to run Proton Mail Bridge. On Mac or
|
||||||
Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API
|
Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API
|
||||||
(e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/))
|
(e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/))
|
||||||
or
|
or
|
||||||
@ -48,9 +48,6 @@ major problems.
|
|||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
### Bridge application
|
|
||||||
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
|
|
||||||
|
|
||||||
### Dev build or run
|
### Dev build or run
|
||||||
- `APP_VERSION`: set the bridge app version used during testing or building
|
- `APP_VERSION`: set the bridge app version used during testing or building
|
||||||
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
|
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
|
||||||
@ -62,35 +59,34 @@ major problems.
|
|||||||
- `TAGS`: set build tags for tests
|
- `TAGS`: set build tags for tests
|
||||||
- `FEATURES`: set feature dir, file or scenario to test
|
- `FEATURES`: set feature dir, file or scenario to test
|
||||||
|
|
||||||
|
## Folders
|
||||||
|
|
||||||
|
There are now three types of system folders which Bridge recognises:
|
||||||
|
|
||||||
|
| | Windows | Mac | Linux | Linux (XDG) |
|
||||||
|
|--------|-------------------------------------|-----------------------------------------------------|-------------------------------------|---------------------------------------|
|
||||||
|
| config | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.config/protonmail/bridge-v3 | $XDG_CONFIG_HOME/protonmail/bridge-v3 |
|
||||||
|
| cache | %LOCALAPPDATA%\protonmail\bridge-v3 | ~/Library/Caches/protonmail/bridge-v3 | ~/.cache/protonmail/bridge-v3 | $XDG_CACHE_HOME/protonmail/bridge-v3 |
|
||||||
|
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
|
||||||
|
| temp | %LOCALAPPDATA%\Temp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
### Database
|
|
||||||
The database stores metadata necessary for presenting messages and mailboxes to an email client:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
|
|
||||||
|
|
||||||
### Preferences
|
| | Base Dir | Path |
|
||||||
User preferences are stored in json at the following location:
|
|------------------------|----------|----------------------------|
|
||||||
- Linux: `~/.config/protonmail/bridge/prefs.json`
|
| bridge lock file | cache | bridge.lock |
|
||||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/prefs.json`
|
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||||
- Windows: `%APPDATA%\protonmail\bridge\prefs.json`
|
| vault | config | vault.enc |
|
||||||
|
| gRPC server json | config | grpcServerConfig.json |
|
||||||
|
| gRPC client json | config | grpcClientConfig_<id>.json |
|
||||||
|
| gRPC Focus server json | config | grpcFocusServerConfig.json |
|
||||||
|
| Logs | data | logs |
|
||||||
|
| gluon DB | data | gluon/backend/db |
|
||||||
|
| gluon messages | data | gluon/backend/store |
|
||||||
|
| Update files | data | updates |
|
||||||
|
| sentry cache | data | sentry_cache |
|
||||||
|
| Mac/Linux File Socket | temp | bridge{4_DIGITS} |
|
||||||
|
|
||||||
### IMAP Cache
|
|
||||||
The currently subscribed mailboxes are held in a json file:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
|
|
||||||
|
|
||||||
### Lock file
|
|
||||||
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
|
|
||||||
|
|
||||||
### TLS Certificate and Key
|
|
||||||
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
|
|
||||||
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
|
|
||||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
|
|
||||||
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`
|
|
||||||
|
|
||||||
|
|||||||
72
ci/build.yml
Normal file
72
ci/build.yml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
.script-build:
|
||||||
|
stage: build
|
||||||
|
needs: ["lint"]
|
||||||
|
extends:
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- which go && go version
|
||||||
|
- which gcc && gcc --version
|
||||||
|
- which qmake && qmake --version
|
||||||
|
- git rev-parse --short=10 HEAD
|
||||||
|
- make build
|
||||||
|
- git diff && git diff-index --quiet HEAD
|
||||||
|
- make vault-editor
|
||||||
|
- make bridge-rollout
|
||||||
|
artifacts:
|
||||||
|
expire_in: 1 day
|
||||||
|
when: always
|
||||||
|
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||||
|
paths:
|
||||||
|
- bridge_*.tgz
|
||||||
|
- vault-editor
|
||||||
|
- bridge-rollout
|
||||||
|
build-linux:
|
||||||
|
extends:
|
||||||
|
- .script-build
|
||||||
|
- .env-linux-build
|
||||||
|
|
||||||
|
build-linux-qa:
|
||||||
|
extends:
|
||||||
|
- build-linux
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
variables:
|
||||||
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
|
build-darwin:
|
||||||
|
extends:
|
||||||
|
- .script-build
|
||||||
|
- .env-darwin
|
||||||
|
|
||||||
|
build-darwin-qa:
|
||||||
|
extends:
|
||||||
|
- build-darwin
|
||||||
|
variables:
|
||||||
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
extends:
|
||||||
|
- .script-build
|
||||||
|
- .env-windows
|
||||||
|
|
||||||
|
build-windows-qa:
|
||||||
|
extends:
|
||||||
|
- build-windows
|
||||||
|
variables:
|
||||||
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
|
trigger-qa-installer:
|
||||||
|
stage: build
|
||||||
|
needs: ["lint"]
|
||||||
|
extends:
|
||||||
|
- .rules-br-tag-always-branch-and-MR-manual
|
||||||
|
variables:
|
||||||
|
APP: bridge
|
||||||
|
WORKFLOW: build-all
|
||||||
|
SRC_TAG: $CI_COMMIT_BRANCH
|
||||||
|
TAG: $CI_COMMIT_TAG
|
||||||
|
SRC_HASH: $CI_COMMIT_SHA
|
||||||
|
trigger:
|
||||||
|
project: "jcuth/bridge-release"
|
||||||
|
branch: master
|
||||||
59
ci/env.yml
Normal file
59
ci/env.yml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
.env-windows:
|
||||||
|
extends:
|
||||||
|
- .image-windows-virt-build
|
||||||
|
before_script:
|
||||||
|
- !reference [.before-script-windows-virt-build, before_script]
|
||||||
|
- !reference [.before-script-git-config, before_script]
|
||||||
|
- mkdir -p .cache/bin
|
||||||
|
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||||
|
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||||
|
variables:
|
||||||
|
GOARCH: amd64
|
||||||
|
BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
|
||||||
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
|
cache:
|
||||||
|
key: windows-vcpkg-go-0
|
||||||
|
paths:
|
||||||
|
- .cache
|
||||||
|
when: 'always'
|
||||||
|
|
||||||
|
.env-darwin:
|
||||||
|
extends:
|
||||||
|
- .image-darwin-build
|
||||||
|
before_script:
|
||||||
|
- !reference [.before-script-darwin-tart-build, before_script]
|
||||||
|
- !reference [.before-script-git-config, before_script]
|
||||||
|
- mkdir -p .cache/bin
|
||||||
|
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||||
|
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||||
|
variables:
|
||||||
|
BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
|
||||||
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
|
cache:
|
||||||
|
key: darwin-go-and-vcpkg
|
||||||
|
paths:
|
||||||
|
- .cache
|
||||||
|
when: 'always'
|
||||||
|
|
||||||
|
.env-linux-build:
|
||||||
|
extends:
|
||||||
|
- .image-linux-build
|
||||||
|
variables:
|
||||||
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
|
cache:
|
||||||
|
key: linux-vcpkg
|
||||||
|
paths:
|
||||||
|
- .cache
|
||||||
|
when: 'always'
|
||||||
|
before_script:
|
||||||
|
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||||
|
- !reference [.before-script-git-config, before_script]
|
||||||
|
- mkdir -p .cache/bin
|
||||||
|
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||||
|
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||||
|
tags:
|
||||||
|
- shared-large
|
||||||
|
|
||||||
25
ci/report.yml
Normal file
25
ci/report.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
include:
|
||||||
|
- project: 'tpe/testmo-reporter'
|
||||||
|
ref: master
|
||||||
|
file: '/scenarios/testmo-script.yml'
|
||||||
|
|
||||||
|
testmo-upload:
|
||||||
|
stage: report
|
||||||
|
extends:
|
||||||
|
- .testmo-upload
|
||||||
|
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||||
|
needs:
|
||||||
|
- test-integration-nightly
|
||||||
|
before_script: []
|
||||||
|
variables:
|
||||||
|
TESTMO_TOKEN: "$TESTMO_TOKEN"
|
||||||
|
TESTMO_URL: "https://proton.testmo.net"
|
||||||
|
PROJECT_ID: "9"
|
||||||
|
NAME: "Nightly integration tests"
|
||||||
|
MILESTONE: "Nightly integration tests"
|
||||||
|
SOURCE: "test-integration-nightly"
|
||||||
|
TAGS: "$CI_COMMIT_REF_SLUG"
|
||||||
|
RESULT_FOLDER: "tests/result/*.xml"
|
||||||
58
ci/rules.yml
Normal file
58
ci/rules.yml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
.rules-branch-and-MR-manual:
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
.rules-branch-manual-MR-and-devel-always:
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
.rules-branch-manual-br-tag-and-MR-and-devel-always:
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG =~ /^br-\d+/
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
.rules-branch-manual-scheduled-and-test-branch-always:
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
.rules-br-tag-always-branch-and-MR-manual:
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == 'push' && $CI_COMMIT_BRANCH
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG =~ /^br-\d+/
|
||||||
|
when: always
|
||||||
|
- when: never
|
||||||
|
|
||||||
7
ci/setup.yml
Normal file
7
ci/setup.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
include:
|
||||||
|
- project: 'go/bridge-internal'
|
||||||
|
ref: 'master'
|
||||||
|
file: 'ci/runners-setup.yml'
|
||||||
|
|
||||||
153
ci/test.yml
Normal file
153
ci/test.yml
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .image-linux-test
|
||||||
|
- .rules-branch-manual-br-tag-and-MR-and-devel-always
|
||||||
|
script:
|
||||||
|
- make lint
|
||||||
|
tags:
|
||||||
|
- shared-medium
|
||||||
|
|
||||||
|
lint-bug-report-preview:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .image-linux-test
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make lint-bug-report-preview
|
||||||
|
tags:
|
||||||
|
- shared-medium
|
||||||
|
|
||||||
|
.script-test:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
script:
|
||||||
|
- which go && go version
|
||||||
|
- which gcc && gcc --version
|
||||||
|
- make test
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- coverage/**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test-linux:
|
||||||
|
extends:
|
||||||
|
- .image-linux-test
|
||||||
|
- .script-test
|
||||||
|
tags:
|
||||||
|
- shared-large
|
||||||
|
|
||||||
|
test-windows:
|
||||||
|
extends:
|
||||||
|
- .env-windows
|
||||||
|
- .script-test
|
||||||
|
|
||||||
|
test-darwin:
|
||||||
|
extends:
|
||||||
|
- .env-darwin
|
||||||
|
- .script-test
|
||||||
|
|
||||||
|
fuzz-linux:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .image-linux-test
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
script:
|
||||||
|
- make fuzz
|
||||||
|
tags:
|
||||||
|
- shared-large
|
||||||
|
|
||||||
|
test-linux-race:
|
||||||
|
extends:
|
||||||
|
- test-linux
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make test-race
|
||||||
|
|
||||||
|
test-integration:
|
||||||
|
extends:
|
||||||
|
- test-linux
|
||||||
|
script:
|
||||||
|
- make test-integration | tee -a integration-job.log
|
||||||
|
after_script:
|
||||||
|
- |
|
||||||
|
grep "Error: " integration-job.log
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- integration-job.log
|
||||||
|
|
||||||
|
test-integration-race:
|
||||||
|
extends:
|
||||||
|
- test-integration
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make test-integration-race | tee -a integration-race-job.log
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- integration-race-job.log
|
||||||
|
|
||||||
|
|
||||||
|
test-integration-nightly:
|
||||||
|
extends:
|
||||||
|
- test-integration
|
||||||
|
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||||
|
needs:
|
||||||
|
- test-integration
|
||||||
|
script:
|
||||||
|
- make test-integration-nightly | tee -a nightly-job.log
|
||||||
|
after_script:
|
||||||
|
- |
|
||||||
|
grep "Error: " nightly-job.log
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- tests/result/feature-tests.xml
|
||||||
|
- nightly-job.log
|
||||||
|
|
||||||
|
test-coverage:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .image-linux-test
|
||||||
|
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||||
|
script:
|
||||||
|
- ./utils/coverage.sh
|
||||||
|
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||||
|
needs:
|
||||||
|
- test-linux
|
||||||
|
- test-windows
|
||||||
|
- test-darwin
|
||||||
|
- test-integration
|
||||||
|
- test-integration-nightly
|
||||||
|
tags:
|
||||||
|
- shared-small
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- coverage*
|
||||||
|
- coverage/**
|
||||||
|
when: 'always'
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
go-vuln-check:
|
||||||
|
extends:
|
||||||
|
- .image-linux-test
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
stage: test
|
||||||
|
tags:
|
||||||
|
- shared-medium
|
||||||
|
script:
|
||||||
|
- ./utils/govulncheck.sh
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
paths:
|
||||||
|
- vulns*
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -19,11 +19,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -44,7 +50,72 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
|
appErr := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
|
||||||
logrus.Fatal(err)
|
if appErr != nil {
|
||||||
|
_ = app.WithLocations(func(l *locations.Locations) error {
|
||||||
|
logsPath, err := l.ProvideLogsPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the session ID if its specified
|
||||||
|
var sessionID logging.SessionID
|
||||||
|
if flagVal, found := getFlagValue(os.Args, app.FlagSessionID); found {
|
||||||
|
sessionID = logging.SessionID(flagVal)
|
||||||
|
} else {
|
||||||
|
sessionID = logging.NewSessionID()
|
||||||
|
}
|
||||||
|
|
||||||
|
closer, err := logging.Init(
|
||||||
|
logsPath,
|
||||||
|
sessionID,
|
||||||
|
logging.BridgeShortAppName,
|
||||||
|
logging.DefaultMaxLogFileSize,
|
||||||
|
logging.DefaultPruningSize,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = logging.Close(closer)
|
||||||
|
}()
|
||||||
|
|
||||||
|
logrus.
|
||||||
|
WithField("appName", constants.FullAppName).
|
||||||
|
WithField("version", constants.Version).
|
||||||
|
WithField("revision", constants.Revision).
|
||||||
|
WithField("tag", constants.Tag).
|
||||||
|
WithField("build", constants.BuildTime).
|
||||||
|
WithField("runtime", runtime.GOOS).
|
||||||
|
WithField("args", os.Args).
|
||||||
|
WithField("SentryID", sentry.GetProtectedHostname()).WithError(appErr).Error("Failed to initialize bridge")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getFlagValue - obtains the value of a specified tag
|
||||||
|
// The flag can be of the following form `-flag value`, `--flag value`, `-flag=value` or `--flags=value`.
|
||||||
|
func getFlagValue(argList []string, flag string) (string, bool) {
|
||||||
|
eqPrefix1 := "-" + flag + "="
|
||||||
|
eqPrefix2 := "--" + flag + "="
|
||||||
|
|
||||||
|
for i := 0; i < len(argList); i++ {
|
||||||
|
arg := argList[i]
|
||||||
|
if strings.HasPrefix(arg, eqPrefix1) {
|
||||||
|
val := strings.TrimPrefix(arg, eqPrefix1)
|
||||||
|
return val, len(val) > 0
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(arg, eqPrefix2) {
|
||||||
|
val := strings.TrimPrefix(arg, eqPrefix2)
|
||||||
|
return val, len(val) > 0
|
||||||
|
}
|
||||||
|
if (arg == "-"+flag || arg == "--"+flag) && i+1 < len(argList) {
|
||||||
|
return argList[i+1], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|||||||
47
cmd/Desktop-Bridge/main_test.go
Normal file
47
cmd/Desktop-Bridge/main_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetFlagValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
args []string
|
||||||
|
flag string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{[]string{"session-id", ""}, "session-id", ""},
|
||||||
|
{[]string{"-session-id", ""}, "session-id", ""},
|
||||||
|
{[]string{"--session-id", ""}, "session-id", ""},
|
||||||
|
{[]string{"session-id", "test"}, "session-id", ""},
|
||||||
|
{[]string{"-session-id", "test"}, "session-id", "test"},
|
||||||
|
{[]string{"--session-id", "test"}, "session-id", "test"},
|
||||||
|
{[]string{"session-id=test"}, "session-id", ""},
|
||||||
|
{[]string{"-session-id=test"}, "session-id", "test"},
|
||||||
|
{[]string{"--session-id=test"}, "session-id", "test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
val, _ := getFlagValue(tt.args, tt.flag)
|
||||||
|
require.Equal(t, val, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,13 +18,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||||
@ -39,30 +40,36 @@ import (
|
|||||||
"github.com/elastic/go-sysinfo/types"
|
"github.com/elastic/go-sysinfo/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/sys/execabs"
|
"golang.org/x/sys/execabs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
appName = "Proton Mail Launcher"
|
appName = "Proton Mail Launcher"
|
||||||
exeName = "bridge"
|
exeName = "bridge"
|
||||||
guiName = "bridge-gui"
|
guiName = "bridge-gui"
|
||||||
|
launcherName = "launcher"
|
||||||
|
|
||||||
FlagCLI = "cli"
|
FlagCLI = "cli"
|
||||||
FlagCLIShort = "c"
|
FlagCLIShort = "c"
|
||||||
FlagNonInteractive = "noninteractive"
|
FlagNonInteractive = "noninteractive"
|
||||||
FlagNonInteractiveShort = "n"
|
FlagNonInteractiveShort = "n"
|
||||||
FlagLauncher = "--launcher"
|
FlagLauncher = "launcher"
|
||||||
FlagWait = "--wait"
|
FlagWait = "wait"
|
||||||
|
FlagSessionID = "session-id"
|
||||||
|
HyphenatedFlagLauncher = "--" + FlagLauncher
|
||||||
|
HyphenatedFlagWait = "--" + FlagWait
|
||||||
|
HyphenatedFlagSessionID = "--" + FlagSessionID
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() { //nolint:funlen
|
func main() { //nolint:funlen
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
l := logrus.WithField("launcher_version", constants.Version)
|
l := logrus.WithField("launcher_version", constants.Version)
|
||||||
|
|
||||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
reporter := sentry.NewReporter(appName, useragent.New())
|
||||||
|
|
||||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||||
defer crashHandler.HandlePanic()
|
defer async.HandlePanic(crashHandler)
|
||||||
|
|
||||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,12 +82,26 @@ func main() { //nolint:funlen
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Fatal("Failed to get logs path")
|
l.WithError(err).Fatal("Failed to get logs path")
|
||||||
}
|
}
|
||||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
|
||||||
|
|
||||||
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
|
sessionID := logging.NewSessionID()
|
||||||
|
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
|
||||||
|
|
||||||
|
var closer io.Closer
|
||||||
|
if closer, err = logging.Init(
|
||||||
|
logsPath,
|
||||||
|
sessionID,
|
||||||
|
logging.LauncherShortAppName,
|
||||||
|
logging.DefaultMaxLogFileSize,
|
||||||
|
logging.NoPruning,
|
||||||
|
os.Getenv("VERBOSITY"),
|
||||||
|
); err != nil {
|
||||||
l.WithError(err).Fatal("Failed to setup logging")
|
l.WithError(err).Fatal("Failed to setup logging")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = logging.Close(closer)
|
||||||
|
}()
|
||||||
|
|
||||||
updatesPath, err := locations.ProvideUpdatesPath()
|
updatesPath, err := locations.ProvideUpdatesPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Fatal("Failed to get updates path")
|
l.WithError(err).Fatal("Failed to get updates path")
|
||||||
@ -107,7 +128,7 @@ func main() { //nolint:funlen
|
|||||||
|
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
|
|
||||||
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr, reporter)
|
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exeToLaunch := guiName
|
exeToLaunch := guiName
|
||||||
if inCLIMode(args) {
|
if inCLIMode(args) {
|
||||||
@ -127,12 +148,14 @@ func main() { //nolint:funlen
|
|||||||
|
|
||||||
l = l.WithField("exe_path", exe)
|
l = l.WithField("exe_path", exe)
|
||||||
|
|
||||||
args, wait, mainExe := findAndStripWait(args)
|
args, wait, mainExes := findAndStripWait(args)
|
||||||
if wait {
|
if wait {
|
||||||
waitForProcessToFinish(mainExe)
|
for _, mainExe := range mainExes {
|
||||||
|
waitForProcessToFinish(mainExe)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
cmd := execabs.Command(exe, appendLauncherPath(launcher, appendOrModifySessionID(args, string(sessionID)))...) //nolint:gosec
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@ -154,19 +177,14 @@ func main() { //nolint:funlen
|
|||||||
|
|
||||||
// appendLauncherPath add launcher path if missing.
|
// appendLauncherPath add launcher path if missing.
|
||||||
func appendLauncherPath(path string, args []string) []string {
|
func appendLauncherPath(path string, args []string) []string {
|
||||||
if !sliceContains(args, FlagLauncher) {
|
if !slices.Contains(args, HyphenatedFlagLauncher) {
|
||||||
res := append([]string{}, args...)
|
res := append([]string{}, args...)
|
||||||
res = append(res, FlagLauncher, path)
|
res = append(res, HyphenatedFlagLauncher, path)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
// sliceContains checks if a value is present in a list.
|
|
||||||
func sliceContains[T comparable](list []T, s T) bool {
|
|
||||||
return xslices.Any(list, func(arg T) bool { return arg == s })
|
|
||||||
}
|
|
||||||
|
|
||||||
// inCLIMode detect if CLI mode is asked.
|
// inCLIMode detect if CLI mode is asked.
|
||||||
func inCLIMode(args []string) bool {
|
func inCLIMode(args []string) bool {
|
||||||
return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort)
|
return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort)
|
||||||
@ -174,7 +192,12 @@ func inCLIMode(args []string) bool {
|
|||||||
|
|
||||||
// hasFlag checks if a flag is present in a list.
|
// hasFlag checks if a flag is present in a list.
|
||||||
func hasFlag(args []string, flag string) bool {
|
func hasFlag(args []string, flag string) bool {
|
||||||
return xslices.Any(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) })
|
return flagIndex(args, flag) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// flagIndex returns the position of the first occurrence of a flag int args, or -1 if the flag is not present.
|
||||||
|
func flagIndex(args []string, flag string) int {
|
||||||
|
return slices.IndexFunc(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// findAndStrip check if a value is present in s list and remove all occurrences of the value from this list.
|
// findAndStrip check if a value is present in s list and remove all occurrences of the value from this list.
|
||||||
@ -186,35 +209,52 @@ func findAndStrip[T comparable](slice []T, v T) (strippedList []T, found bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findAndStripWait Check for waiter flag get its value and clean them both.
|
// findAndStripWait Check for waiter flag get its value and clean them both.
|
||||||
func findAndStripWait(args []string) ([]string, bool, string) {
|
func findAndStripWait(args []string) ([]string, bool, []string) {
|
||||||
res := append([]string{}, args...)
|
res := append([]string{}, args...)
|
||||||
|
|
||||||
hasFlag := false
|
hasFlag := false
|
||||||
var value string
|
values := make([]string, 0)
|
||||||
|
|
||||||
for k, v := range res {
|
for k, v := range res {
|
||||||
if v != FlagWait {
|
if v != HyphenatedFlagWait {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if k+1 >= len(res) {
|
if k+1 >= len(res) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hasFlag = true
|
hasFlag = true
|
||||||
value = res[k+1]
|
values = append(values, res[k+1])
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasFlag {
|
if hasFlag {
|
||||||
res, _ = findAndStrip(res, FlagWait)
|
res, _ = findAndStrip(res, HyphenatedFlagWait)
|
||||||
res, _ = findAndStrip(res, value)
|
for _, v := range values {
|
||||||
|
res, _ = findAndStrip(res, v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res, hasFlag, value
|
return res, hasFlag, values
|
||||||
|
}
|
||||||
|
|
||||||
|
// return args with the sessionID flag and value added or modified. The original slice is not modified.
|
||||||
|
func appendOrModifySessionID(args []string, sessionID string) []string {
|
||||||
|
index := flagIndex(args, FlagSessionID)
|
||||||
|
if index < 0 {
|
||||||
|
return append(args, HyphenatedFlagSessionID, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if index == len(args)-1 {
|
||||||
|
return append(args, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := slices.Clone(args)
|
||||||
|
res[index+1] = sessionID
|
||||||
|
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPathToUpdatedExecutable(
|
func getPathToUpdatedExecutable(
|
||||||
name string,
|
name string,
|
||||||
ver *versioner.Versioner,
|
ver *versioner.Versioner,
|
||||||
kr *crypto.KeyRing,
|
kr *crypto.KeyRing,
|
||||||
reporter *sentry.Reporter,
|
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
versions, err := ver.ListVersions()
|
versions, err := ver.ListVersions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -236,10 +276,6 @@ func getPathToUpdatedExecutable(
|
|||||||
if err := version.VerifyFiles(kr); err != nil {
|
if err := version.VerifyFiles(kr); err != nil {
|
||||||
vlog.WithError(err).Error("Files failed verification and will be removed")
|
vlog.WithError(err).Error("Files failed verification and will be removed")
|
||||||
|
|
||||||
if err := reporter.ReportMessage(fmt.Sprintf("version %v failed verification: %v", version, err)); err != nil {
|
|
||||||
vlog.WithError(err).Error("Failed to report corrupt update files")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := version.Remove(); err != nil {
|
if err := version.Remove(); err != nil {
|
||||||
vlog.WithError(err).Error("Failed to remove files")
|
vlog.WithError(err).Error("Failed to remove files")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -20,39 +20,62 @@ package main
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSliceContains(t *testing.T) {
|
|
||||||
assert.True(t, sliceContains([]string{"a", "b", "c"}, "a"))
|
|
||||||
assert.True(t, sliceContains([]int{1, 2, 3}, 2))
|
|
||||||
assert.False(t, sliceContains([]string{"a", "b", "c"}, "A"))
|
|
||||||
assert.False(t, sliceContains([]int{1, 2, 3}, 4))
|
|
||||||
assert.False(t, sliceContains([]string{}, "a"))
|
|
||||||
assert.True(t, sliceContains([]string{"a", "a"}, "a"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindAndStrip(t *testing.T) {
|
func TestFindAndStrip(t *testing.T) {
|
||||||
list := []string{"a", "b", "c", "c", "b", "c"}
|
list := []string{"a", "b", "c", "c", "b", "c"}
|
||||||
|
|
||||||
result, found := findAndStrip(list, "a")
|
result, found := findAndStrip(list, "a")
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"b", "c", "c", "b", "c"}))
|
assert.Equal(t, result, []string{"b", "c", "c", "b", "c"})
|
||||||
|
|
||||||
result, found = findAndStrip(list, "c")
|
result, found = findAndStrip(list, "c")
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{"a", "b", "b"}))
|
assert.Equal(t, result, []string{"a", "b", "b"})
|
||||||
|
|
||||||
result, found = findAndStrip([]string{"c", "c", "c"}, "c")
|
result, found = findAndStrip([]string{"c", "c", "c"}, "c")
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{}))
|
assert.Equal(t, result, []string{})
|
||||||
|
|
||||||
result, found = findAndStrip(list, "A")
|
result, found = findAndStrip(list, "A")
|
||||||
assert.False(t, found)
|
assert.False(t, found)
|
||||||
assert.True(t, xslices.Equal(result, list))
|
assert.Equal(t, result, list)
|
||||||
|
|
||||||
result, found = findAndStrip([]string{}, "a")
|
result, found = findAndStrip([]string{}, "a")
|
||||||
assert.False(t, found)
|
assert.False(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{}))
|
assert.Equal(t, result, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindAndStripWait(t *testing.T) {
|
||||||
|
result, found, values := findAndStripWait([]string{"a", "b", "c"})
|
||||||
|
assert.False(t, found)
|
||||||
|
assert.Equal(t, result, []string{"a", "b", "c"})
|
||||||
|
assert.Equal(t, values, []string{})
|
||||||
|
|
||||||
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, result, []string{"a"})
|
||||||
|
assert.Equal(t, values, []string{"b"})
|
||||||
|
|
||||||
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, result, []string{"a"})
|
||||||
|
assert.Equal(t, values, []string{"b", "c"})
|
||||||
|
|
||||||
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, result, []string{"a"})
|
||||||
|
assert.Equal(t, values, []string{"b", "c", "d"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendOrModifySessionID(t *testing.T) {
|
||||||
|
sessionID := string(logging.NewSessionID())
|
||||||
|
assert.Equal(t, appendOrModifySessionID(nil, sessionID), []string{"--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{}, sessionID), []string{"--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--cli"}, sessionID), []string{"--cli", "--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
|
||||||
|
assert.Equal(t, appendOrModifySessionID([]string{"--session-id", "<oldID>", "--cli"}, sessionID), []string{"--session-id", sessionID, "--cli"})
|
||||||
}
|
}
|
||||||
|
|||||||
135
doc/bridge.md
135
doc/bridge.md
@ -1,135 +0,0 @@
|
|||||||
# Bridge
|
|
||||||
|
|
||||||
## Main blocks
|
|
||||||
|
|
||||||
This is basic overview of the main bridge blocks.
|
|
||||||
|
|
||||||
Note connection between IMAP/SMTP and PMAPI. IMAP and SMTP packages are in the queue to be refactored
|
|
||||||
and we would like to try to have functionality in bridge core or bridge utilities (such as messages)
|
|
||||||
than direct usage of PMAPI from IMAP or SMTP. Also database (BoltDB) should be moved to bridge core.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
S[Server]
|
|
||||||
C[Client]
|
|
||||||
U[User]
|
|
||||||
|
|
||||||
subgraph "Bridge app"
|
|
||||||
Core[Bridge core]
|
|
||||||
API[PMAPI]
|
|
||||||
Store
|
|
||||||
DB[BoltDB]
|
|
||||||
Frontend["Qt / CLI"]
|
|
||||||
IMAP
|
|
||||||
SMTP
|
|
||||||
|
|
||||||
IMAP --> Store
|
|
||||||
IMAP --> Core
|
|
||||||
SMTP --> Core
|
|
||||||
SMTP --> API
|
|
||||||
Core --> API
|
|
||||||
Core --> Store
|
|
||||||
Store --> API
|
|
||||||
Store --> DB
|
|
||||||
Frontend --> Core
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
C --> IMAP
|
|
||||||
C --> SMTP
|
|
||||||
U --> Frontend
|
|
||||||
API --> S
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code structure
|
|
||||||
|
|
||||||
More detailed graph of main types used in Bridge app and connection between them. Here is already
|
|
||||||
communication to PMAPI only from bridge core which is not true, yet. IMAP and SMTP are still calling
|
|
||||||
PMAPI directly.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
|
|
||||||
C["Client (e.g. Thunderbird)"]
|
|
||||||
PM[Proton Mail Server]
|
|
||||||
|
|
||||||
subgraph "Bridge app"
|
|
||||||
subgraph "Bridge core"
|
|
||||||
B[Bridge]
|
|
||||||
U[User]
|
|
||||||
|
|
||||||
B --> U
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Store
|
|
||||||
StoreU[Store User]
|
|
||||||
StoreA[Address]
|
|
||||||
StoreM[Mailbox]
|
|
||||||
|
|
||||||
StoreU --> StoreA
|
|
||||||
StoreA --> StoreM
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Credentials
|
|
||||||
CredStore[Store]
|
|
||||||
Creds[Credentials]
|
|
||||||
|
|
||||||
CredStore --> Creds
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Frontend
|
|
||||||
CLI
|
|
||||||
Qt
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph IMAP
|
|
||||||
IB[IMAP backend]
|
|
||||||
IA[IMAP address]
|
|
||||||
IM[IMAP mailbox]
|
|
||||||
|
|
||||||
IB --> B
|
|
||||||
IB --> IA
|
|
||||||
IA --> IM
|
|
||||||
IA --> U
|
|
||||||
IA --> StoreA
|
|
||||||
IM --> StoreM
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph SMTP
|
|
||||||
SB[SMTP backend]
|
|
||||||
SS[SMTP session]
|
|
||||||
|
|
||||||
SB --> B
|
|
||||||
SB --> SS
|
|
||||||
SS --> U
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph PMAPI
|
|
||||||
AC[Client]
|
|
||||||
end
|
|
||||||
|
|
||||||
C --> IB
|
|
||||||
C --> SB
|
|
||||||
|
|
||||||
CLI --> B
|
|
||||||
Qt --> B
|
|
||||||
|
|
||||||
U --> CredStore
|
|
||||||
U --> Creds
|
|
||||||
|
|
||||||
U --> StoreU
|
|
||||||
|
|
||||||
StoreU --> AC
|
|
||||||
StoreA --> AC
|
|
||||||
StoreM --> AC
|
|
||||||
|
|
||||||
B --> AC
|
|
||||||
U --> AC
|
|
||||||
|
|
||||||
AC --> PM
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to debug
|
|
||||||
|
|
||||||
Run `make run-debug` which starts [Delve](https://github.com/go-delve/delve).
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
# Communication
|
|
||||||
|
|
||||||
## First login and sync
|
|
||||||
|
|
||||||
When user logs in to the bridge for the first time, immediatelly starts the first sync.
|
|
||||||
First sync downloads all headers of all e-mails and creates database to have proper UIDs
|
|
||||||
and indexes for IMAP. See [database](database.md) for more information.
|
|
||||||
|
|
||||||
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
|
|
||||||
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
|
|
||||||
and lables) without need to download each e-mail headers many times.
|
|
||||||
|
|
||||||
Note that we need to download also bodies to calculate size of the e-mail and set proper
|
|
||||||
content type (clients uses content type for guess if e-mail contains attachment)--but only
|
|
||||||
body, not attachment. Also it's downloaded only for the first time. After that we store
|
|
||||||
those information in our database so next time we only sync headers, labels and so on.
|
|
||||||
|
|
||||||
First sync takes some time. List of 150 messages takes about second and then we need to
|
|
||||||
download bodies for each message. We still need to do some optimalizations. Anyway, if
|
|
||||||
user has reasonable amount of e-mails, there is good chance user will see e-mails in the
|
|
||||||
client right after adding account.
|
|
||||||
|
|
||||||
When account is added to client, client start the sync. This sync will ask Bridge app
|
|
||||||
for all headers (done quickly) and then starts to download all bodies and attachment.
|
|
||||||
Unfortunatelly for some e-mail more than once if the same e-mail is in more mailboxes
|
|
||||||
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
|
|
||||||
|
|
||||||
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
|
|
||||||
servers (each 30 seconds) for new updates (new message, keys, …).
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant S as Server
|
|
||||||
participant B as Bridge
|
|
||||||
participant C as Client
|
|
||||||
|
|
||||||
Note right of B: Set up PM account<br/>by user
|
|
||||||
|
|
||||||
loop First sync
|
|
||||||
B ->> S: Fetch body and attachements
|
|
||||||
Note right of B: Build local database<br/>(e-mail UIDs)
|
|
||||||
end
|
|
||||||
|
|
||||||
Note right of C: Set up IMAP/SMTP<br/>by user
|
|
||||||
|
|
||||||
C ->> B: IMAP login
|
|
||||||
B ->> S: Authenticate user
|
|
||||||
Note right of B: Create IMAP user
|
|
||||||
|
|
||||||
loop Event loop, every 30 sec
|
|
||||||
B ->> S: Fetch e-mail headers
|
|
||||||
B ->> C: Send IMAP IDLE response
|
|
||||||
end
|
|
||||||
|
|
||||||
C ->> B: IMAP LIST directories
|
|
||||||
|
|
||||||
loop Client sync
|
|
||||||
C ->> B: IMAP SELECT directory
|
|
||||||
C ->> B: IMAP SEARCH e-mails UIDs
|
|
||||||
C ->> B: IMAP FETCH of e-mail UID
|
|
||||||
B ->> S: Fetch body and attachements
|
|
||||||
Note right of B: Decrypt message<br/>and attachement
|
|
||||||
B ->> C: IMAP response
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## IMAP IDLE extension
|
|
||||||
|
|
||||||
IMAP IDLE is extension, it has to be supported by both client and server. IMAP server (in our case
|
|
||||||
the bridge) supports it so clients can use it. It works by issuing `IDLE` command by the client and
|
|
||||||
keeps the connection open. When the server has some update, server (the bridge) will respond to that
|
|
||||||
by `EXISTS` (new message), `APPEND` (imported message), `EXPUNGE` (deleted message) or `MOVE` response.
|
|
||||||
|
|
||||||
Even when there is connection with IDLE open, server can mark the client as inactive. Therefore,
|
|
||||||
it's recommended the client should reissue the connection after each 29 minutes. This is not the
|
|
||||||
real push and can fail!
|
|
||||||
|
|
||||||
Our event loop is also simple pull and it will trigger IMAP IDLE when we get some new update from
|
|
||||||
the server. Would be good to have push from the server, but we need to wait for the support on API.
|
|
||||||
|
|
||||||
RFC: https://tools.ietf.org/html/rfc2177
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant S as Server
|
|
||||||
participant B as Bridge
|
|
||||||
participant C as Client
|
|
||||||
|
|
||||||
C ->> B: IMAP IDLE
|
|
||||||
|
|
||||||
loop Every 30 seconds
|
|
||||||
S ->> B: Checking events
|
|
||||||
B ->> C: IMAP response
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sending e-mails
|
|
||||||
|
|
||||||
E-mail are sent over standard SMTP protocol. Our bridge takes the message, encrypts and sent it
|
|
||||||
further to our server which will then send the message to its final destination. The important
|
|
||||||
and tricky part is encryption. See [encryption](encryption.md) or [PMEL document](https://docs.google.com/document/d/1lEBkG0DC5FOWlumInKtu4a9Cc1Eszp48ZhFy9UpPQso/edit)
|
|
||||||
for more information.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant S as Server
|
|
||||||
participant B as Bridge
|
|
||||||
participant C as Client
|
|
||||||
|
|
||||||
C ->> B: SMTP send e-mail
|
|
||||||
Note right of B: Encrypt messages
|
|
||||||
B ->> S: Send encrypted e-mail
|
|
||||||
B ->> C: Respond OK
|
|
||||||
```
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# Database
|
|
||||||
|
|
||||||
Bridge needs to have a small database to pair our IDs with IMAP UIDs and indexes. IMAP protocol
|
|
||||||
requires every message to have an unique UID in mailbox. In this context, mailbox is not an account,
|
|
||||||
but a folder or label. This means that one message can have more UIDs, one for each mailbox (folder),
|
|
||||||
and that two messages can have the same UID, but each for different mailbox (folder).
|
|
||||||
|
|
||||||
IMAP index is just an index. Look at it like to an array: `["UID1", "UID2", "UID3"]`. We can access
|
|
||||||
message by UID or index; for example index 2 and UID `UID2`. When this message is deleted, we need
|
|
||||||
to re-index all following messages. The array will look now like `["UID1", "UID3"]` and the last
|
|
||||||
message can be accessed by index 2 or UID `UID3`.
|
|
||||||
|
|
||||||
See RFCs for more information:
|
|
||||||
|
|
||||||
* https://tools.ietf.org/html/rfc822
|
|
||||||
* https://tools.ietf.org/html/rfc3501
|
|
||||||
|
|
||||||
Our database is currently built on BBolt and have those buckets (key-value storage):
|
|
||||||
|
|
||||||
* Message metadata bucket:
|
|
||||||
|
|
||||||
* `[metadataBucket][API_ID] -> pmapi.Message{subject, from, to, size, other headers...}` (without body or attachment)
|
|
||||||
|
|
||||||
* Mapping buckets
|
|
||||||
|
|
||||||
* `[mailboxesBucket][addressID-mailboxID][api_ids][API_ID] -> UID`
|
|
||||||
* `[mailboxesBucket][addressID-mailboxID][imap_ids][UID] -> API_ID`
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
# Encryption
|
|
||||||
|
|
||||||
Encryption is done in PMAPI, bridge utils and bridge itself. The best would be to keep encryption
|
|
||||||
in PMAPI and bridge utils (in package such as messages). All packages are using our high-level
|
|
||||||
GopenPGP library on top of OpenPGP.
|
|
||||||
|
|
||||||
## `gopenpgp.KeyRing`
|
|
||||||
|
|
||||||
We use one `KeyRing` per address. Our usage then contains all keys for specific address. Primary
|
|
||||||
key is always on the first position, then there old ones to be able to decrypt last e-mail.
|
|
||||||
OpenPGP encrypts given message with all available keys, so we need to first get first (primary)
|
|
||||||
key for encryption to have message encrypted only once with primary key.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
# Bridge Documentation
|
|
||||||
|
|
||||||
Documentation pages in order to read for a novice:
|
|
||||||
|
|
||||||
* [Bridge code](bridge.md)
|
|
||||||
* [Internal Bridge database](database.md)
|
|
||||||
* [Communication between Bridge, Client and Server](communication.md)
|
|
||||||
* [Encryption](encryption.md)
|
|
||||||
|
|
||||||
103
doc/updates.md
103
doc/updates.md
@ -1,103 +0,0 @@
|
|||||||
# Update mechanism of Bridge
|
|
||||||
|
|
||||||
There are mulitple options how to change version of application:
|
|
||||||
* Automatic in-app update
|
|
||||||
* Manual in-app update
|
|
||||||
* Manual install
|
|
||||||
|
|
||||||
In-app update ends with restarting bridge into new version. Automatic in-app
|
|
||||||
update is downloading, verifying and installing the new version immediatelly
|
|
||||||
without user confirmation. For manual in-app update user needs to confirm first.
|
|
||||||
Update is done from special update file published on website.
|
|
||||||
|
|
||||||
The manual installation requires user to download, verify and install manually
|
|
||||||
using installer for given OS.
|
|
||||||
|
|
||||||
The bridge is installed and executed differently for given OS:
|
|
||||||
|
|
||||||
* Windows and Linux apps are using launcher mechanism:
|
|
||||||
* There is system protected installation path which is created on first
|
|
||||||
install. It contains bridge exe and launcher exe. When users starts
|
|
||||||
bridge the launcher is executed first. It will check update path compare
|
|
||||||
version with installed one. The newer version then is then executed.
|
|
||||||
* Update mechanism means to replace files in update folder which is located
|
|
||||||
in user space.
|
|
||||||
|
|
||||||
* macOS app does not use launcher
|
|
||||||
* No launcher, only one executable
|
|
||||||
* In-App udpate replaces the bridge files in installation path directly
|
|
||||||
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph Frontend
|
|
||||||
U[User requests<br>version check]
|
|
||||||
ManIns((Notify user about<br>manual install<br>is needed))
|
|
||||||
R((Notify user<br>about restart))
|
|
||||||
ManUp((Notify user about<br>manual update))
|
|
||||||
NF((Notify user about<br>force update))
|
|
||||||
|
|
||||||
ManUp -->|Install| InstFront[Install]
|
|
||||||
InstFront -->|Ok| R
|
|
||||||
InstFront -->|Error| ManIns
|
|
||||||
|
|
||||||
U --> CheckFront[Check online]
|
|
||||||
CheckFront -->|Ok| IAFront{Is new version<br>and applicable?}
|
|
||||||
CheckFront -->|Error| ManIns
|
|
||||||
|
|
||||||
IAFront -->|No| Latest((Notify user<br>has latest version))
|
|
||||||
IAFront -->|Yes| CanInstall{Can update?}
|
|
||||||
CanInstall -->|No| ManIns
|
|
||||||
CanInstall -->|Yes| NotifOrInstall{Is automatic<br>update enabled?}
|
|
||||||
NotifOrInstall -->|Manual| ManUp
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
subgraph Backend
|
|
||||||
W[Wait for next check]
|
|
||||||
|
|
||||||
W --> Check[Check online]
|
|
||||||
|
|
||||||
Check --> NV{Has new<br>version?}
|
|
||||||
Check -->|Error| W
|
|
||||||
NV -->|No new version| W
|
|
||||||
IA{Is install<br>applicable?}
|
|
||||||
NV -->|New version<br>available| IA
|
|
||||||
IA -->|Local rollout<br>not enough| W
|
|
||||||
IA -->|Yes| AU{Is automatic\nupdate enabled?}
|
|
||||||
|
|
||||||
AU -->|Yes| CanUp{Can update?}
|
|
||||||
CanUp -->|No| ManIns
|
|
||||||
|
|
||||||
CanUp -->|Yes| Ins[Install]
|
|
||||||
Ins -->|Error| ManIns
|
|
||||||
Ins -->|Ok| R
|
|
||||||
|
|
||||||
AU -->|No| ManUp
|
|
||||||
ManUp -->|Ignore| W
|
|
||||||
|
|
||||||
|
|
||||||
F[Force update]
|
|
||||||
F --> NF
|
|
||||||
end
|
|
||||||
|
|
||||||
ManIns --> Web[Open web page]
|
|
||||||
NF --> Web
|
|
||||||
ManUp --> Web
|
|
||||||
R --> Re[Restart]
|
|
||||||
NF --> Q[Quit bridge]
|
|
||||||
NotifOrInstall -->|Automatic| W
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
The non-trivial is to combine the update with setting change:
|
|
||||||
* turn off/on automatic in-app updates
|
|
||||||
* change from stable to beta or back
|
|
||||||
|
|
||||||
_TODO fill flow chart details_
|
|
||||||
|
|
||||||
|
|
||||||
We are not support downgrade functionality. Only some circumstances can lead to
|
|
||||||
downgrading the app version.
|
|
||||||
|
|
||||||
_TODO fill flow chart details_
|
|
||||||
2
extern/vcpkg
vendored
2
extern/vcpkg
vendored
Submodule extern/vcpkg updated: f93ba152d5...fba75d0906
133
go.mod
133
go.mod
@ -1,125 +1,138 @@
|
|||||||
module github.com/ProtonMail/proton-bridge/v3
|
module github.com/ProtonMail/proton-bridge/v3
|
||||||
|
|
||||||
go 1.18
|
go 1.23
|
||||||
|
|
||||||
|
toolchain go1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.1.1
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e
|
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/PuerkitoBio/goquery v1.8.0
|
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||||
github.com/bradenaw/juniper v0.8.0
|
github.com/bradenaw/juniper v0.12.0
|
||||||
github.com/cucumber/godog v0.12.5
|
github.com/cucumber/godog v0.12.5
|
||||||
github.com/cucumber/messages-go/v16 v16.0.1
|
github.com/cucumber/messages-go/v16 v16.0.1
|
||||||
github.com/docker/docker-credential-helpers v0.6.3
|
github.com/docker/docker-credential-helpers v0.8.1
|
||||||
github.com/elastic/go-sysinfo v1.8.1
|
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542
|
||||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||||
github.com/emersion/go-message v0.16.0
|
github.com/emersion/go-message v0.16.0
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
||||||
|
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3
|
||||||
github.com/fatih/color v1.13.0
|
github.com/fatih/color v1.13.0
|
||||||
github.com/getsentry/sentry-go v0.15.0
|
github.com/getsentry/sentry-go v0.15.0
|
||||||
github.com/go-resty/resty/v2 v2.7.0
|
github.com/go-resty/resty/v2 v2.7.0
|
||||||
github.com/goccy/go-json v0.9.11
|
|
||||||
github.com/godbus/dbus v4.1.0+incompatible
|
github.com/godbus/dbus v4.1.0+incompatible
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.6.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||||
|
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173
|
||||||
github.com/keybase/go-keychain v0.0.0
|
github.com/keybase/go-keychain v0.0.0
|
||||||
github.com/miekg/dns v1.1.50
|
github.com/miekg/dns v1.1.50
|
||||||
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/profile v1.6.0
|
github.com/pkg/profile v1.7.0
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.2
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/urfave/cli/v2 v2.20.3
|
github.com/urfave/cli/v2 v2.24.4
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.uber.org/goleak v1.2.0
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||||
golang.org/x/net v0.1.0
|
golang.org/x/net v0.34.0
|
||||||
golang.org/x/sys v0.1.0
|
golang.org/x/oauth2 v0.7.0
|
||||||
golang.org/x/text v0.4.0
|
golang.org/x/sys v0.29.0
|
||||||
google.golang.org/grpc v1.50.1
|
golang.org/x/text v0.21.0
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/api v0.114.0
|
||||||
|
google.golang.org/grpc v1.56.3
|
||||||
|
google.golang.org/protobuf v1.33.0
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
ariga.io/atlas v0.7.0 // indirect
|
cloud.google.com/go/compute v1.19.1 // indirect
|
||||||
entgo.io/ent v0.11.2 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 // indirect
|
github.com/ProtonMail/go-crypto v1.1.4-proton // indirect
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // indirect
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||||
github.com/ProtonMail/go-srp v0.0.5 // indirect
|
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
|
||||||
github.com/chzyer/test v1.0.0 // indirect
|
github.com/chzyer/test v1.0.0 // indirect
|
||||||
github.com/cloudflare/circl v1.2.0 // indirect
|
github.com/cloudflare/circl v1.5.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
github.com/danieljoos/wincred v1.2.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/elastic/go-windows v1.0.1 // indirect
|
github.com/elastic/go-windows v1.0.1 // indirect
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||||
github.com/hashicorp/hcl/v2 v2.14.0 // indirect
|
|
||||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.2 // indirect
|
github.com/rivo/uniseg v0.4.2 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
github.com/zclconf/go-cty v1.11.0 // indirect
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||||
golang.org/x/crypto v0.1.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
|
||||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
|
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe
|
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
||||||
|
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77
|
||||||
)
|
)
|
||||||
|
|||||||
445
go.sum
445
go.sum
@ -1,5 +1,3 @@
|
|||||||
ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k=
|
|
||||||
ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
|
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
@ -7,67 +5,70 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
|
|||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
|
||||||
|
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||||
|
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||||
|
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
entgo.io/ent v0.11.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto=
|
fyne.io/fyne v1.4.2/go.mod h1:xL4c3WmpE/Tvz5CEm5vqsaizU/EeOCm9DYlL2GtTSiM=
|
||||||
entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU=
|
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/GF/+H8/9udc9QAgieFr+jr1tjXlJo35RAhsUbWY=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
|
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||||
|
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2 h1:lDgMidI/9j2eedavcy7YICv8+F73ooVTUoUGBE4dO0s=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/gluon v0.17.1-0.20250116113909-2ebd96ec0bc2/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e h1://xRNjGTAMXw2U91MtqPc4krUtxQmt2+4z1oYrBaOWU=
|
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
|
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
|
||||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3 h1:YYnLBVcg7WrEbYVmF1PBr4AEQlob9rCphsMHAmF4CAo=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250121114701-67bd01ad0bc3/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||||
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4 h1:xCot3copmyPz0cDOwl1XVmYQDRJGi6EgJUKJ58Vn58U=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c h1:dxnbB+ov77BDj1LC35fKZ14hLoTpU6OTpZySwxarVx0=
|
||||||
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI=
|
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton h1:MMVgE6nk5Ulh9Ud5L1Xc5iaPKE85FbfKQV17ZMucrR0=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc=
|
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton/go.mod h1:+PjybET6fgcLzldFy1hpy7s8VibZ0T1hLFbxnnMk0lo=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
|
||||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
|
||||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
@ -75,19 +76,29 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
|||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||||
github.com/bradenaw/juniper v0.8.0 h1:sdanLNdJbLjcLj993VYIwUHlUVkLzvgiD/x9O7cvvxk=
|
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
||||||
github.com/bradenaw/juniper v0.8.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||||
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
|
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||||
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
|
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
@ -96,7 +107,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||||
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
|
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
|
||||||
@ -106,76 +116,98 @@ github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T
|
|||||||
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||||
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
|
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
|
||||||
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||||
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc=
|
github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77 h1:sdB/yJMbubPQothFl6KYCOrMBRgy0pZbBXIWoJqSFLo=
|
||||||
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
|
github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
|
||||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
|
||||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
|
||||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4=
|
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||||
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
|
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||||
|
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
|
||||||
|
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||||
|
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||||
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542 h1:IFTm6NBbfSgZCaeEzorQhH4T7ZERl4j+1u7oXWzmJcM=
|
||||||
|
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
||||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317 h1:i0cBrdFLm8A/3hWEjn/BwdXLBplFJoZtu63p7bjrmaI=
|
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
||||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
|
|
||||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=
|
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
|
||||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
|
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/fyne-io/mobile v0.1.2-0.20201127155338-06aeb98410cc/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
|
||||||
|
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
||||||
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
|
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
|
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
|
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
|
||||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
|
||||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
|
||||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
|
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
@ -184,25 +216,43 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev
|
|||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
@ -239,19 +289,22 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hashicorp/hcl/v2 v2.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc=
|
|
||||||
github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
|
||||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
|
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||||
|
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173 h1:jOONCXyzHWM+ukp+weX77o//U3pMeOj62CNxChJLxIU=
|
||||||
|
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173/go.mod h1:uO/uctjf8AcWhNfp5Ili6oPtyFrAoQXEtVY3N798VkQ=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
@ -261,19 +314,22 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
|||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
@ -282,13 +338,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||||
@ -297,8 +354,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
|
|||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
|
||||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
@ -310,21 +365,32 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
|||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||||
|
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||||
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
|
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
@ -332,32 +398,30 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
|||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
||||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
@ -372,28 +436,35 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
|||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||||
|
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
|
||||||
|
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/urfave/cli/v2 v2.20.3 h1:lOgGidH/N5loaigd9HjFsOIhXSTrzl7tBpHswZ428w4=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
|
||||||
|
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
@ -401,57 +472,63 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
|
|||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k=
|
||||||
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e h1:SkwG94eNiiYJhbeDE018Grw09HIN/KB9NlRmZsrzfWs=
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -466,25 +543,38 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
|
||||||
|
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -494,40 +584,62 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -538,19 +650,23 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
|
|||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@ -560,10 +676,14 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E
|
|||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
|
||||||
|
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
@ -573,20 +693,35 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||||
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
|
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||||
|
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
@ -598,17 +733,17 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -19,25 +19,32 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
|
||||||
|
"github.com/elastic/go-sysinfo"
|
||||||
"github.com/pkg/profile"
|
"github.com/pkg/profile"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@ -48,6 +55,9 @@ const (
|
|||||||
flagCPUProfile = "cpu-prof"
|
flagCPUProfile = "cpu-prof"
|
||||||
flagCPUProfileShort = "p"
|
flagCPUProfileShort = "p"
|
||||||
|
|
||||||
|
flagTraceProfile = "trace-prof"
|
||||||
|
flagTraceProfileShort = "t"
|
||||||
|
|
||||||
flagMemProfile = "mem-prof"
|
flagMemProfile = "mem-prof"
|
||||||
flagMemProfileShort = "m"
|
flagMemProfileShort = "m"
|
||||||
|
|
||||||
@ -65,21 +75,46 @@ const (
|
|||||||
|
|
||||||
flagLogIMAP = "log-imap"
|
flagLogIMAP = "log-imap"
|
||||||
flagLogSMTP = "log-smtp"
|
flagLogSMTP = "log-smtp"
|
||||||
|
|
||||||
|
flagEnableKeychainTest = "enable-keychain-test"
|
||||||
|
flagDisableKeychainTest = "disable-keychain-test"
|
||||||
|
|
||||||
|
flagSoftwareRenderer = "software-renderer"
|
||||||
|
flagSetSoftwareRenderer = "set-software-renderer"
|
||||||
|
flagSetHardwareRenderer = "set-hardware-renderer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hidden flags.
|
// Hidden flags.
|
||||||
const (
|
const (
|
||||||
flagLauncher = "launcher"
|
flagLauncher = "launcher"
|
||||||
flagNoWindow = "no-window"
|
flagNoWindow = "no-window"
|
||||||
flagParentPID = "parent-pid"
|
flagParentPID = "parent-pid"
|
||||||
flagSoftwareRenderer = "software-renderer"
|
FlagSessionID = "session-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||||
|
appShortName = "bridge"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() *cli.App { //nolint:funlen
|
// the two flags below have been deprecated by BRIDGE-281. We however keep them so that bridge does not error if they are passed on startup.
|
||||||
|
var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||||
|
Name: flagEnableKeychainTest,
|
||||||
|
Usage: "This flag is deprecated and does nothing",
|
||||||
|
Value: false,
|
||||||
|
DisableDefaultText: true,
|
||||||
|
Hidden: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||||
|
Name: flagDisableKeychainTest,
|
||||||
|
Usage: "This flag is deprecated and does nothing",
|
||||||
|
Value: false,
|
||||||
|
DisableDefaultText: true,
|
||||||
|
Hidden: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *cli.App {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
|
|
||||||
app.Name = constants.FullAppName
|
app.Name = constants.FullAppName
|
||||||
@ -90,6 +125,11 @@ func New() *cli.App { //nolint:funlen
|
|||||||
Aliases: []string{flagCPUProfileShort},
|
Aliases: []string{flagCPUProfileShort},
|
||||||
Usage: "Generate CPU profile",
|
Usage: "Generate CPU profile",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: flagTraceProfile,
|
||||||
|
Aliases: []string{flagTraceProfileShort},
|
||||||
|
Usage: "Generate Trace profile",
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: flagMemProfile,
|
Name: flagMemProfile,
|
||||||
Aliases: []string{flagMemProfileShort},
|
Aliases: []string{flagMemProfileShort},
|
||||||
@ -123,6 +163,24 @@ func New() *cli.App { //nolint:funlen
|
|||||||
Name: flagLogSMTP,
|
Name: flagLogSMTP,
|
||||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
|
||||||
|
Usage: "Use software rendering of the GUI for the current execution of the application",
|
||||||
|
Value: false,
|
||||||
|
DisableDefaultText: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: flagSetSoftwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
|
||||||
|
Usage: "Toggle software rendering of the GUI for the current and future executions of the application",
|
||||||
|
Value: false,
|
||||||
|
DisableDefaultText: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: flagSetHardwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
|
||||||
|
Usage: "Toggle hardware rendering of the GUI for the current and future executions of the application",
|
||||||
|
Value: false,
|
||||||
|
DisableDefaultText: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Hidden flags
|
// Hidden flags
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
@ -141,20 +199,32 @@ func New() *cli.App { //nolint:funlen
|
|||||||
Hidden: true,
|
Hidden: true,
|
||||||
Value: -1,
|
Value: -1,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.StringFlag{
|
||||||
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
|
Name: FlagSessionID,
|
||||||
Usage: "GUI is using software renderer",
|
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Value: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We override the default help value because we want "Show" to be capitalized
|
||||||
|
cli.HelpFlag = &cli.BoolFlag{
|
||||||
|
Name: "help",
|
||||||
|
Aliases: []string{"h"},
|
||||||
|
Usage: "Show help",
|
||||||
|
DisableDefaultText: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if onMacOS() {
|
||||||
|
// The two flags below were introduced for BRIDGE-116, and are available only on macOS.
|
||||||
|
// They have been later removed fro BRIDGE-281.
|
||||||
|
app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
|
||||||
|
}
|
||||||
|
|
||||||
app.Action = run
|
app.Action = run
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(c *cli.Context) error { //nolint:funlen
|
func run(c *cli.Context) error {
|
||||||
// Get the current bridge version.
|
// Get the current bridge version.
|
||||||
version, err := semver.NewVersion(constants.Version)
|
version, err := semver.NewVersion(constants.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -165,7 +235,7 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
identifier := useragent.New()
|
identifier := useragent.New()
|
||||||
|
|
||||||
// Create a new Sentry client that will be used to report crashes etc.
|
// Create a new Sentry client that will be used to report crashes etc.
|
||||||
reporter := sentry.NewReporter(constants.FullAppName, constants.Version, identifier)
|
reporter := sentry.NewReporter(constants.FullAppName, identifier)
|
||||||
|
|
||||||
// Determine the exe that should be used to restart/autostart the app.
|
// Determine the exe that should be used to restart/autostart the app.
|
||||||
// By default, this is the launcher, if used. Otherwise, we try to get
|
// By default, this is the launcher, if used. Otherwise, we try to get
|
||||||
@ -180,14 +250,19 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
exe = os.Args[0]
|
exe = os.Args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
migrationErr := migrateOldVersions()
|
var logCloser io.Closer
|
||||||
|
defer func() {
|
||||||
|
_ = logging.Close(logCloser)
|
||||||
|
}()
|
||||||
|
|
||||||
// Run with profiling if requested.
|
// Restart the app if requested.
|
||||||
return withProfiler(c, func() error {
|
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||||
// Restart the app if requested.
|
// Handle crashes with various actions.
|
||||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||||
// Handle crashes with various actions.
|
migrationErr := migrateOldVersions()
|
||||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
|
||||||
|
// Run with profiling if requested.
|
||||||
|
return withProfiler(c, func() error {
|
||||||
// Load the locations where we store our files.
|
// Load the locations where we store our files.
|
||||||
return WithLocations(func(locations *locations.Locations) error {
|
return WithLocations(func(locations *locations.Locations) error {
|
||||||
// Migrate the keychain helper.
|
// Migrate the keychain helper.
|
||||||
@ -196,49 +271,71 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize logging.
|
// Initialize logging.
|
||||||
return withLogging(c, crashHandler, locations, func() error {
|
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
|
||||||
|
logCloser = closer
|
||||||
|
|
||||||
// If there was an error during migration, log it now.
|
// If there was an error during migration, log it now.
|
||||||
if migrationErr != nil {
|
if migrationErr != nil {
|
||||||
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we are the only instance running.
|
// Ensure we are the only instance running.
|
||||||
return withSingleInstance(locations, version, func() error {
|
settings, err := locations.ProvideSettingsPath()
|
||||||
// Unlock the encrypted vault.
|
if err != nil {
|
||||||
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
logrus.WithError(err).Error("Failed to get settings path")
|
||||||
if !vault.Migrated() {
|
}
|
||||||
// Migrate old settings into the vault.
|
|
||||||
if err := migrateOldSettings(vault); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to migrate old settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate old accounts into the vault.
|
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||||
if err := migrateOldAccounts(locations, vault); err != nil {
|
// Look for available keychains
|
||||||
logrus.WithError(err).Error("Failed to migrate old accounts")
|
return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
|
||||||
}
|
// Unlock the encrypted vault.
|
||||||
|
return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||||
// The vault has been migrated.
|
if !v.Migrated() {
|
||||||
if err := vault.SetMigrated(); err != nil {
|
// Migrate old settings into the vault.
|
||||||
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
if err := migrateOldSettings(v); err != nil {
|
||||||
}
|
logrus.WithError(err).Error("Failed to migrate old settings")
|
||||||
}
|
|
||||||
|
|
||||||
// Load the cookies from the vault.
|
|
||||||
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
|
|
||||||
// Create a new bridge instance.
|
|
||||||
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, vault, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
|
||||||
if insecure {
|
|
||||||
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
|
||||||
b.PushError(bridge.ErrVaultInsecure)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if corrupt {
|
// Migrate old accounts into the vault.
|
||||||
logrus.Warn("The vault is corrupt and has been wiped")
|
if err := migrateOldAccounts(locations, keychains, v); err != nil {
|
||||||
b.PushError(bridge.ErrVaultCorrupt)
|
logrus.WithError(err).Error("Failed to migrate old accounts")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the frontend.
|
// The vault has been migrated.
|
||||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
if err := v.SetMigrated(); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"lastVersion": v.GetLastVersion().String(),
|
||||||
|
"showAllMail": v.GetShowAllMail(),
|
||||||
|
"updateCh": v.GetUpdateChannel(),
|
||||||
|
"autoUpdate": v.GetAutoUpdate(),
|
||||||
|
"rollout": v.GetUpdateRollout(),
|
||||||
|
"DoH": v.GetProxyAllowed(),
|
||||||
|
}).Info("Vault loaded")
|
||||||
|
|
||||||
|
// Load the cookies from the vault.
|
||||||
|
return withCookieJar(v, func(cookieJar http.CookieJar) error {
|
||||||
|
// Create a new bridge instance.
|
||||||
|
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, keychains, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
||||||
|
if insecure {
|
||||||
|
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
||||||
|
b.PushError(bridge.ErrVaultInsecure)
|
||||||
|
}
|
||||||
|
|
||||||
|
if corrupt {
|
||||||
|
logrus.Warn("The vault is corrupt and has been wiped")
|
||||||
|
b.PushError(bridge.ErrVaultCorrupt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old updates files
|
||||||
|
b.RemoveOldUpdates()
|
||||||
|
|
||||||
|
// Run the frontend.
|
||||||
|
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -248,18 +345,25 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// if an error occurs, it must be logged now because we're about to close the log file.
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's another instance already running, try to raise it and exit.
|
// If there's another instance already running, try to raise it and exit.
|
||||||
func withSingleInstance(locations *locations.Locations, version *semver.Version, fn func() error) error {
|
func withSingleInstance(settingPath, lockFile string, version *semver.Version, fn func() error) error {
|
||||||
logrus.Debug("Checking for other instances")
|
logrus.Debug("Checking for other instances")
|
||||||
defer logrus.Debug("Single instance stopped")
|
defer logrus.Debug("Single instance stopped")
|
||||||
|
|
||||||
lock, err := checkSingleInstance(locations.GetLockFile(), version)
|
lock, err := checkSingleInstance(settingPath, lockFile, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Info("Another instance is already running; raising it")
|
logrus.Info("Another instance is already running; raising it")
|
||||||
|
|
||||||
if ok := focus.TryRaise(); !ok {
|
if ok := focus.TryRaise(settingPath); !ok {
|
||||||
return fmt.Errorf("another instance is already running but it could not be raised")
|
return fmt.Errorf("another instance is already running but it could not be raised")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,7 +382,7 @@ func withSingleInstance(locations *locations.Locations, version *semver.Version,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize our logging system.
|
// Initialize our logging system.
|
||||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
|
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
|
||||||
logrus.Debug("Initializing logging")
|
logrus.Debug("Initializing logging")
|
||||||
defer logrus.Debug("Logging stopped")
|
defer logrus.Debug("Logging stopped")
|
||||||
|
|
||||||
@ -291,23 +395,52 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
|||||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||||
|
|
||||||
// Initialize logging.
|
// Initialize logging.
|
||||||
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
|
sessionID := logging.NewSessionIDFromString(c.String(FlagSessionID))
|
||||||
|
var closer io.Closer
|
||||||
|
if closer, err = logging.Init(
|
||||||
|
logsPath,
|
||||||
|
sessionID,
|
||||||
|
logging.BridgeShortAppName,
|
||||||
|
logging.DefaultMaxLogFileSize,
|
||||||
|
logging.DefaultPruningSize,
|
||||||
|
c.String(flagLogLevel),
|
||||||
|
); err != nil {
|
||||||
return fmt.Errorf("could not initialize logging: %w", err)
|
return fmt.Errorf("could not initialize logging: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we dump a stack trace if we crash.
|
// Ensure we dump a stack trace if we crash.
|
||||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
|
||||||
|
|
||||||
logrus.
|
logrus.
|
||||||
WithField("appName", constants.FullAppName).
|
WithField("appName", constants.FullAppName).
|
||||||
WithField("version", constants.Version).
|
WithField("version", constants.Version).
|
||||||
WithField("revision", constants.Revision).
|
WithField("revision", constants.Revision).
|
||||||
|
WithField("tag", constants.Tag).
|
||||||
WithField("build", constants.BuildTime).
|
WithField("build", constants.BuildTime).
|
||||||
WithField("runtime", runtime.GOOS).
|
WithField("runtime", runtime.GOOS).
|
||||||
WithField("args", os.Args).
|
WithField("args", os.Args).
|
||||||
|
WithField("SentryID", sentry.GetProtectedHostname()).
|
||||||
Info("Run app")
|
Info("Run app")
|
||||||
|
|
||||||
return fn()
|
now := time.Now()
|
||||||
|
logrus.
|
||||||
|
WithField("timeZone", now.Format("MST")).
|
||||||
|
WithField("offset", now.Format("-07:00:00")).
|
||||||
|
Info("Time zone info")
|
||||||
|
|
||||||
|
host, err := sysinfo.Host()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("Could not retrieve operating system info")
|
||||||
|
} else {
|
||||||
|
osInfo := host.Info().OS
|
||||||
|
logrus.
|
||||||
|
WithField("name", osInfo.Name).
|
||||||
|
WithField("version", osInfo.Version).
|
||||||
|
WithField("build", osInfo.Build).
|
||||||
|
Info("Operating system info")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(closer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithLocations provides access to locations where we store our files.
|
// WithLocations provides access to locations where we store our files.
|
||||||
@ -322,14 +455,7 @@ func WithLocations(fn func(*locations.Locations) error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new locations object that will be used to provide paths to store files.
|
// Create a new locations object that will be used to provide paths to store files.
|
||||||
locations := locations.New(provider, constants.ConfigName)
|
return fn(locations.New(provider, constants.ConfigName))
|
||||||
defer func() {
|
|
||||||
if err := locations.Clean(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to clean locations")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return fn(locations)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start profiling if requested.
|
// Start profiling if requested.
|
||||||
@ -341,6 +467,11 @@ func withProfiler(c *cli.Context, fn func() error) error {
|
|||||||
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Bool(flagTraceProfile) {
|
||||||
|
logrus.Debug("Running with Trace profiling")
|
||||||
|
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
|
||||||
|
}
|
||||||
|
|
||||||
if c.Bool(flagMemProfile) {
|
if c.Bool(flagMemProfile) {
|
||||||
logrus.Debug("Running with memory profiling")
|
logrus.Debug("Running with memory profiling")
|
||||||
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
||||||
@ -366,7 +497,7 @@ func withCrashHandler(restarter *restarter.Restarter, reporter *sentry.Reporter,
|
|||||||
defer logrus.Debug("Crash handler stopped")
|
defer logrus.Debug("Crash handler stopped")
|
||||||
|
|
||||||
crashHandler := crash.NewHandler(crash.ShowErrorNotification(constants.FullAppName))
|
crashHandler := crash.NewHandler(crash.ShowErrorNotification(constants.FullAppName))
|
||||||
defer crashHandler.HandlePanic()
|
defer async.HandlePanic(crashHandler)
|
||||||
|
|
||||||
// On crash, send crash report to Sentry.
|
// On crash, send crash report to Sentry.
|
||||||
crashHandler.AddRecoveryAction(reporter.ReportException)
|
crashHandler.AddRecoveryAction(reporter.ReportException)
|
||||||
@ -403,6 +534,10 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
|||||||
return fmt.Errorf("could not create cookie jar: %w", err)
|
return fmt.Errorf("could not create cookie jar: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := setDeviceCookies(persister); err != nil {
|
||||||
|
return fmt.Errorf("could not set device cookies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Persist the cookies to the vault when we close.
|
// Persist the cookies to the vault when we close.
|
||||||
defer func() {
|
defer func() {
|
||||||
logrus.Debug("Persisting cookies")
|
logrus.Debug("Persisting cookies")
|
||||||
@ -414,3 +549,33 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
|||||||
|
|
||||||
return fn(persister)
|
return fn(persister)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithKeychainList init the list of usable keychains.
|
||||||
|
func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error {
|
||||||
|
logrus.Debug("Creating keychain list")
|
||||||
|
defer logrus.Debug("Keychain list stop")
|
||||||
|
defer async.HandlePanic(panicHandler)
|
||||||
|
return fn(keychain.NewList())
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDeviceCookies(jar *cookies.Jar) error {
|
||||||
|
url, err := url.Parse(constants.APIHost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range map[string]string{
|
||||||
|
"hhn": sentry.GetProtectedHostname(),
|
||||||
|
"tz": sentry.GetTimeZone(),
|
||||||
|
"lng": sentry.GetSystemLang(),
|
||||||
|
"clr": string(theme.DefaultTheme()),
|
||||||
|
} {
|
||||||
|
jar.SetCookies(url, []*http.Cookie{{Name: name, Value: value, Secure: true}})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onMacOS() bool {
|
||||||
|
return runtime.GOOS == "darwin"
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/go-autostart"
|
"github.com/ProtonMail/go-autostart"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
@ -36,17 +37,16 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const vaultSecretName = "bridge-vault-key"
|
|
||||||
|
|
||||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||||
|
|
||||||
// withBridge creates creates and tears down the bridge.
|
// withBridge creates and tears down the bridge.
|
||||||
func withBridge( //nolint:funlen
|
func withBridge(
|
||||||
c *cli.Context,
|
c *cli.Context,
|
||||||
exe string,
|
exe string,
|
||||||
locations *locations.Locations,
|
locations *locations.Locations,
|
||||||
@ -56,6 +56,7 @@ func withBridge( //nolint:funlen
|
|||||||
reporter *sentry.Reporter,
|
reporter *sentry.Reporter,
|
||||||
vault *vault.Vault,
|
vault *vault.Vault,
|
||||||
cookieJar http.CookieJar,
|
cookieJar http.CookieJar,
|
||||||
|
keychains *keychain.List,
|
||||||
fn func(*bridge.Bridge, <-chan events.Event) error,
|
fn func(*bridge.Bridge, <-chan events.Event) error,
|
||||||
) error {
|
) error {
|
||||||
logrus.Debug("Creating bridge")
|
logrus.Debug("Creating bridge")
|
||||||
@ -79,7 +80,7 @@ func withBridge( //nolint:funlen
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Create a proxy dialer which switches to a proxy if the request fails.
|
// Create a proxy dialer which switches to a proxy if the request fails.
|
||||||
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost)
|
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost, crashHandler)
|
||||||
|
|
||||||
// Create the autostarter.
|
// Create the autostarter.
|
||||||
autostarter := newAutostarter(exe)
|
autostarter := newAutostarter(exe)
|
||||||
@ -98,6 +99,7 @@ func withBridge( //nolint:funlen
|
|||||||
autostarter,
|
autostarter,
|
||||||
updater,
|
updater,
|
||||||
version,
|
version,
|
||||||
|
keychains,
|
||||||
|
|
||||||
// The API stuff.
|
// The API stuff.
|
||||||
constants.APIHost,
|
constants.APIHost,
|
||||||
@ -110,6 +112,8 @@ func withBridge( //nolint:funlen
|
|||||||
// Crash and report stuff
|
// Crash and report stuff
|
||||||
crashHandler,
|
crashHandler,
|
||||||
reporter,
|
reporter,
|
||||||
|
imap.DefaultEpochUIDValidityGenerator(),
|
||||||
|
nil,
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||||
@ -155,7 +159,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return updater.NewUpdater(
|
return updater.NewUpdater(
|
||||||
updater.NewInstaller(versioner.New(updatesDir)),
|
versioner.New(updatesDir),
|
||||||
verifier,
|
verifier,
|
||||||
constants.UpdateName,
|
constants.UpdateName,
|
||||||
runtime.GOOS,
|
runtime.GOOS,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -46,10 +46,11 @@ func runFrontend(
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case c.Bool(flagCLI):
|
case c.Bool(flagCLI):
|
||||||
return bridgeCLI.New(bridge, restarter, eventCh).Loop()
|
return bridgeCLI.New(bridge, restarter, eventCh, crashHandler, quitCh).Loop()
|
||||||
|
|
||||||
case c.Bool(flagNonInteractive):
|
case c.Bool(flagNonInteractive):
|
||||||
select {}
|
<-quitCh
|
||||||
|
return nil
|
||||||
|
|
||||||
case c.Bool(flagGRPC):
|
case c.Bool(flagGRPC):
|
||||||
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, eventCh, quitCh, !c.Bool(flagNoWindow), parentPID)
|
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, eventCh, quitCh, !c.Bool(flagNoWindow), parentPID)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -43,7 +43,7 @@ import (
|
|||||||
|
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
func migrateKeychainHelper(locations *locations.Locations) error {
|
func migrateKeychainHelper(locations *locations.Locations) error {
|
||||||
logrus.Info("Migrating keychain helper")
|
logrus.Trace("Checking if keychain helper needs to be migrated")
|
||||||
|
|
||||||
settings, err := locations.ProvideSettingsPath()
|
settings, err := locations.ProvideSettingsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,7 +75,11 @@ func migrateKeychainHelper(locations *locations.Locations) error {
|
|||||||
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vault.SetHelper(settings, prefs.Helper)
|
err = vault.SetHelper(settings, prefs.Helper)
|
||||||
|
if err == nil {
|
||||||
|
logrus.Info("Keychain helper has been migrated")
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
@ -87,6 +91,11 @@ func migrateOldSettings(v *vault.Vault) error {
|
|||||||
return fmt.Errorf("failed to get user config dir: %w", err)
|
return fmt.Errorf("failed to get user config dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return migrateOldSettingsWithDir(configDir, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gosec
|
||||||
|
func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
|
||||||
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return nil
|
return nil
|
||||||
@ -94,10 +103,30 @@ func migrateOldSettings(v *vault.Vault) error {
|
|||||||
return fmt.Errorf("failed to read old prefs file: %w", err)
|
return fmt.Errorf("failed to read old prefs file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return migratePrefsToVault(v, b)
|
if err := migratePrefsToVault(v, b); err != nil {
|
||||||
|
return fmt.Errorf("failed to migrate prefs to vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("Migrating TLS certificate")
|
||||||
|
|
||||||
|
certPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "cert.pem"))
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to read old cert file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "key.pem"))
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to read old key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List, v *vault.Vault) error {
|
||||||
logrus.Info("Migrating accounts")
|
logrus.Info("Migrating accounts")
|
||||||
|
|
||||||
settings, err := locations.ProvideSettingsPath()
|
settings, err := locations.ProvideSettingsPath()
|
||||||
@ -109,8 +138,7 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get helper: %w", err)
|
return fmt.Errorf("failed to get helper: %w", err)
|
||||||
}
|
}
|
||||||
|
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||||
keychain, err := keychain.NewKeychain(helper, "bridge")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create keychain: %w", err)
|
return fmt.Errorf("failed to create keychain: %w", err)
|
||||||
}
|
}
|
||||||
@ -147,7 +175,12 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
|
|||||||
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := v.AddUser(creds.UserID, creds.Name, authUID, authRef, creds.MailboxPassword)
|
var primaryEmail string
|
||||||
|
if len(creds.EmailList()) > 0 {
|
||||||
|
primaryEmail = creds.EmailList()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := v.AddUser(creds.UserID, creds.Name, primaryEmail, authUID, authRef, creds.MailboxPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to add user %q: %w", userID, err)
|
return fmt.Errorf("failed to add user %q: %w", userID, err)
|
||||||
}
|
}
|
||||||
@ -182,7 +215,6 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
|
||||||
func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
||||||
var prefs struct {
|
var prefs struct {
|
||||||
IMAPPort int `json:"user_port_imap,,string"`
|
IMAPPort int `json:"user_port_imap,,string"`
|
||||||
@ -193,11 +225,10 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||||||
UpdateChannel updater.Channel `json:"update_channel"`
|
UpdateChannel updater.Channel `json:"update_channel"`
|
||||||
UpdateRollout float64 `json:"rollout,,string"`
|
UpdateRollout float64 `json:"rollout,,string"`
|
||||||
|
|
||||||
FirstStart bool `json:"first_time_start,,string"`
|
FirstStart bool `json:"first_time_start,,string"`
|
||||||
FirstStartGUI bool `json:"first_time_start_gui,,string"`
|
ColorScheme string `json:"color_scheme"`
|
||||||
ColorScheme string `json:"color_scheme"`
|
LastVersion *semver.Version `json:"last_used_version"`
|
||||||
LastVersion *semver.Version `json:"last_used_version"`
|
Autostart bool `json:"autostart,,string"`
|
||||||
Autostart bool `json:"autostart,,string"`
|
|
||||||
|
|
||||||
AllowProxy bool `json:"allow_proxy,,string"`
|
AllowProxy bool `json:"allow_proxy,,string"`
|
||||||
FetchWorkers int `json:"fetch_workers,,string"`
|
FetchWorkers int `json:"fetch_workers,,string"`
|
||||||
@ -241,10 +272,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := vault.SetFirstStartGUI(prefs.FirstStartGUI); err != nil {
|
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start GUI: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
|
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
||||||
}
|
}
|
||||||
@ -265,14 +292,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := vault.SetSyncWorkers(prefs.FetchWorkers); err != nil {
|
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync workers: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := vault.SetSyncAttPool(prefs.AttachmentWorkers); err != nil {
|
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync attachment pool: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil {
|
if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -25,6 +25,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||||
@ -34,58 +35,52 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMigratePrefsToVault(t *testing.T) {
|
func TestMigratePrefsToVaultWithKeys(t *testing.T) {
|
||||||
// Create a new vault.
|
// Create a new vault.
|
||||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, corrupt)
|
require.NoError(t, corrupt)
|
||||||
|
|
||||||
// load the old prefs file.
|
// load the old prefs file.
|
||||||
b, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
configDir := filepath.Join("testdata", "with_keys")
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Migrate the old prefs file to the new vault.
|
// Migrate the old prefs file to the new vault.
|
||||||
require.NoError(t, migratePrefsToVault(vault, b))
|
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||||
|
|
||||||
// Check that the IMAP and SMTP prefs are migrated.
|
// Check Json Settings
|
||||||
require.Equal(t, 2143, vault.GetIMAPPort())
|
validateJSONPrefs(t, vault)
|
||||||
require.Equal(t, 2025, vault.GetSMTPPort())
|
|
||||||
require.True(t, vault.GetSMTPSSL())
|
|
||||||
|
|
||||||
// Check that the update channel is migrated.
|
cert, key := vault.GetBridgeTLSCert()
|
||||||
require.True(t, vault.GetAutoUpdate())
|
// Check the keys were found and collected.
|
||||||
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
|
require.Equal(t, "-----BEGIN CERTIFICATE-----", string(cert))
|
||||||
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
|
require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(key))
|
||||||
|
}
|
||||||
|
|
||||||
// Check that the app settings have been migrated.
|
func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
|
||||||
require.False(t, vault.GetFirstStart())
|
// Create a new vault.
|
||||||
require.True(t, vault.GetFirstStartGUI())
|
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||||
require.Equal(t, "blablabla", vault.GetColorScheme())
|
|
||||||
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
|
||||||
require.True(t, vault.GetAutostart())
|
|
||||||
|
|
||||||
// Check that the other app settings have been migrated.
|
|
||||||
require.Equal(t, 16, vault.SyncWorkers())
|
|
||||||
require.Equal(t, 16, vault.SyncAttPool())
|
|
||||||
require.False(t, vault.GetProxyAllowed())
|
|
||||||
require.False(t, vault.GetShowAllMail())
|
|
||||||
|
|
||||||
// Check that the cookies have been migrated.
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, corrupt)
|
||||||
|
|
||||||
cookies, err := cookies.NewCookieJar(jar, vault)
|
// load the old prefs file.
|
||||||
require.NoError(t, err)
|
configDir := filepath.Join("testdata", "without_keys")
|
||||||
|
|
||||||
url, err := url.Parse("https://api.protonmail.ch")
|
// Migrate the old prefs file to the new vault.
|
||||||
require.NoError(t, err)
|
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||||
|
|
||||||
// There should be a cookie for the API.
|
// Migrate the old prefs file to the new vault.
|
||||||
require.NotEmpty(t, cookies.Cookies(url))
|
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||||
|
|
||||||
|
// Check Json Settings
|
||||||
|
validateJSONPrefs(t, vault)
|
||||||
|
|
||||||
|
// Check the keys were found and collected.
|
||||||
|
cert, key := vault.GetBridgeTLSCert()
|
||||||
|
require.NotEqual(t, []byte("-----BEGIN CERTIFICATE-----"), cert)
|
||||||
|
require.NotEqual(t, []byte("-----BEGIN RSA PRIVATE KEY-----"), key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeychainMigration(t *testing.T) {
|
func TestKeychainMigration(t *testing.T) {
|
||||||
@ -102,7 +97,7 @@ func TestKeychainMigration(t *testing.T) {
|
|||||||
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
||||||
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
||||||
|
|
||||||
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "without_keys", "protonmail", "bridge", "prefs.json"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, os.WriteFile(
|
require.NoError(t, os.WriteFile(
|
||||||
@ -137,11 +132,9 @@ func TestKeychainMigration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUserMigration(t *testing.T) {
|
func TestUserMigration(t *testing.T) {
|
||||||
keychainHelper := keychain.NewTestHelper()
|
kcl := keychain.NewTestKeychainsList()
|
||||||
|
|
||||||
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
|
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
|
||||||
|
|
||||||
kc, err := keychain.NewKeychain("mock", "bridge")
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, kc.Put("brokenID", "broken"))
|
require.NoError(t, kc.Put("brokenID", "broken"))
|
||||||
@ -178,11 +171,11 @@ func TestUserMigration(t *testing.T) {
|
|||||||
token, err := crypto.RandomToken(32)
|
token, err := crypto.RandomToken(32)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token)
|
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, corrupt)
|
require.NoError(t, corrupt)
|
||||||
|
|
||||||
require.NoError(t, migrateOldAccounts(locations, v))
|
require.NoError(t, migrateOldAccounts(locations, kcl, v))
|
||||||
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
||||||
|
|
||||||
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
|
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
|
||||||
@ -197,3 +190,38 @@ func TestUserMigration(t *testing.T) {
|
|||||||
require.Equal(t, vault.CombinedMode, u.AddressMode())
|
require.Equal(t, vault.CombinedMode, u.AddressMode())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateJSONPrefs(t *testing.T, vault *vault.Vault) {
|
||||||
|
// Check that the IMAP and SMTP prefs are migrated.
|
||||||
|
require.Equal(t, 2143, vault.GetIMAPPort())
|
||||||
|
require.Equal(t, 2025, vault.GetSMTPPort())
|
||||||
|
require.True(t, vault.GetSMTPSSL())
|
||||||
|
|
||||||
|
// Check that the update channel is migrated.
|
||||||
|
require.True(t, vault.GetAutoUpdate())
|
||||||
|
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
|
||||||
|
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
|
||||||
|
|
||||||
|
// Check that the app settings have been migrated.
|
||||||
|
require.False(t, vault.GetFirstStart())
|
||||||
|
require.Equal(t, "blablabla", vault.GetColorScheme())
|
||||||
|
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
||||||
|
require.True(t, vault.GetAutostart())
|
||||||
|
|
||||||
|
// Check that the other app settings have been migrated.
|
||||||
|
require.False(t, vault.GetProxyAllowed())
|
||||||
|
require.False(t, vault.GetShowAllMail())
|
||||||
|
|
||||||
|
// Check that the cookies have been migrated.
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cookies, err := cookies.NewCookieJar(jar, vault)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
url, err := url.Parse("https://api.protonmail.ch")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// There should be a cookie for the API.
|
||||||
|
require.NotEmpty(t, cookies.Cookies(url))
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -34,17 +34,17 @@ import (
|
|||||||
//
|
//
|
||||||
// For macOS and Linux when already running version is older than this instance
|
// For macOS and Linux when already running version is older than this instance
|
||||||
// it will kill old and continue with this new bridge (i.e. no error returned).
|
// it will kill old and continue with this new bridge (i.e. no error returned).
|
||||||
func checkSingleInstance(lockFilePath string, curVersion *semver.Version) (*os.File, error) {
|
func checkSingleInstance(settingPath, lockFilePath string, curVersion *semver.Version) (*os.File, error) {
|
||||||
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
|
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
|
||||||
logrus.WithField("path", lockFilePath).Debug("Created lock file; no other instance is running")
|
logrus.WithField("path", lockFilePath).Debug("Created lock file; no other instance is running")
|
||||||
return lock, nil
|
return lock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debug("Failed to create lock file; another instance is running")
|
logrus.Warn("Failed to create lock file; another instance is running")
|
||||||
|
|
||||||
// We couldn't create the lock file, so another instance is probably running.
|
// We couldn't create the lock file, so another instance is probably running.
|
||||||
// Check if it's an older version of the app.
|
// Check if it's an older version of the app.
|
||||||
lastVersion, ok := focus.TryVersion()
|
lastVersion, ok := focus.TryVersion(settingPath)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("failed to determine version of running instance")
|
return nil, fmt.Errorf("failed to determine version of running instance")
|
||||||
}
|
}
|
||||||
|
|||||||
1
internal/app/testdata/with_keys/protonmail/bridge/cert.pem
vendored
Normal file
1
internal/app/testdata/with_keys/protonmail/bridge/cert.pem
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
1
internal/app/testdata/with_keys/protonmail/bridge/key.pem
vendored
Normal file
1
internal/app/testdata/with_keys/protonmail/bridge/key.pem
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
31
internal/app/testdata/without_keys/protonmail/bridge/prefs.json
vendored
Normal file
31
internal/app/testdata/without_keys/protonmail/bridge/prefs.json
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"allow_proxy": "false",
|
||||||
|
"attachment_workers": "16",
|
||||||
|
"autostart": "true",
|
||||||
|
"autoupdate": "true",
|
||||||
|
"cache_compression": "true",
|
||||||
|
"cache_concurrent_read": "16",
|
||||||
|
"cache_concurrent_write": "16",
|
||||||
|
"cache_enabled": "true",
|
||||||
|
"cache_location": "/home/user/.config/protonmail/bridge/cache/c11/messages",
|
||||||
|
"cache_min_free_abs": "250000000",
|
||||||
|
"cache_min_free_rat": "",
|
||||||
|
"color_scheme": "blablabla",
|
||||||
|
"cookies": "{\"https://api.protonmail.ch\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.ch\",\"Expires\":\"2023-02-19T00:20:40.269424437+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=blablablablablablablablabla; Domain=protonmail.ch; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"default\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:40.269428627+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=default; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}],\"https://protonmail.com\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.com\",\"Expires\":\"2023-02-19T00:20:18.315084712+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=Y3q2Mh-ClvqL6LWeYdfyPgAAABI; Domain=protonmail.com; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"redirect\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:18.315087646+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=redirect; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}]}",
|
||||||
|
"fetch_workers": "16",
|
||||||
|
"first_time_start": "false",
|
||||||
|
"first_time_start_gui": "true",
|
||||||
|
"imap_workers": "16",
|
||||||
|
"is_all_mail_visible": "false",
|
||||||
|
"last_heartbeat": "325",
|
||||||
|
"last_used_version": "2.3.0+git",
|
||||||
|
"preferred_keychain": "secret-service",
|
||||||
|
"rebranding_migrated": "true",
|
||||||
|
"report_outgoing_email_without_encryption": "false",
|
||||||
|
"rollout": "0.4849529004202015",
|
||||||
|
"user_port_api": "1042",
|
||||||
|
"update_channel": "early",
|
||||||
|
"user_port_imap": "2143",
|
||||||
|
"user_port_smtp": "2025",
|
||||||
|
"user_ssl_smtp": "true"
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,59 +18,50 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool) error) error {
|
func WithVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||||
logrus.Debug("Creating vault")
|
logrus.Debug("Creating vault")
|
||||||
defer logrus.Debug("Vault stopped")
|
defer logrus.Debug("Vault stopped")
|
||||||
|
|
||||||
// Create the encVault.
|
// Create the encVault.
|
||||||
encVault, insecure, corrupt, err := newVault(locations)
|
encVault, insecure, corrupt, err := newVault(reporter, locations, keychains, panicHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not create vault: %w", err)
|
return fmt.Errorf("could not create vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"insecure": insecure,
|
"insecure": insecure,
|
||||||
"corrupt": corrupt,
|
"corrupt": corrupt != nil,
|
||||||
}).Debug("Vault created")
|
}).Debug("Vault created")
|
||||||
|
|
||||||
// Install the certificates if needed.
|
if corrupt != nil {
|
||||||
if installed := encVault.GetCertsInstalled(); !installed {
|
logrus.WithError(corrupt).Warn("Failed to load existing vault, vault has been reset")
|
||||||
logrus.Debug("Installing certificates")
|
|
||||||
|
|
||||||
if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil {
|
|
||||||
return fmt.Errorf("failed to install certs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
|
||||||
return fmt.Errorf("failed to set certs installed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debug("Certificates successfully installed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cert, _ := encVault.GetBridgeTLSCert()
|
||||||
|
certs.NewInstaller().LogCertInstallStatus(cert)
|
||||||
|
|
||||||
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
||||||
|
|
||||||
return fn(encVault, insecure, corrupt)
|
return fn(encVault, insecure, corrupt != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
|
func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
|
||||||
vaultDir, err := locations.ProvideSettingsPath()
|
vaultDir, err := locations.ProvideSettingsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
|
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
|
||||||
@ -80,7 +71,18 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||||||
insecure bool
|
insecure bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if key, err := getVaultKey(vaultDir); err != nil {
|
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
|
||||||
|
if reporter != nil {
|
||||||
|
if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{
|
||||||
|
"keychainDefaultHelper": keychains.GetDefaultHelper(),
|
||||||
|
"keychainUsableHelpersLength": len(keychains.GetHelpers()),
|
||||||
|
"error": err.Error(),
|
||||||
|
}); rerr != nil {
|
||||||
|
logrus.WithError(err).Info("Failed to report keychain issue to Sentry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithError(err).Error("Could not load/create vault key")
|
||||||
insecure = true
|
insecure = true
|
||||||
|
|
||||||
// We store the insecure vault in a separate directory
|
// We store the insecure vault in a separate directory
|
||||||
@ -89,55 +91,39 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||||||
vaultKey = key
|
vaultKey = key
|
||||||
}
|
}
|
||||||
|
|
||||||
gluonDir, err := locations.ProvideGluonPath()
|
gluonCacheDir, err := locations.ProvideGluonCachePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
|
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return vault, insecure, corrupt, nil
|
return vault, insecure, corrupt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getVaultKey(vaultDir string) ([]byte, error) {
|
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
|
||||||
helper, err := vault.GetHelper(vaultDir)
|
helper, err := vault.GetHelper(vaultDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := keychain.List()
|
key, err := vault.GetVaultKey(kc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not list keychain: %w", err)
|
if keychain.IsErrKeychainNoItem(err) {
|
||||||
}
|
logrus.WithError(err).Warn("no vault key found, generating new")
|
||||||
|
return vault.NewVaultKey(kc)
|
||||||
if !slices.Contains(secrets, vaultSecretName) {
|
|
||||||
tok, err := crypto.RandomToken(32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not generate random token: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil {
|
return nil, fmt.Errorf("could not check for vault key: %w", err)
|
||||||
return nil, fmt.Errorf("could not put keychain item: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, keyEnc, err := keychain.Get(vaultSecretName)
|
return key, nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get keychain item: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyDec, err := base64.StdEncoding.DecodeString(keyEnc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not decode keychain item: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyDec, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package async
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Abortable collects groups of functions that can be aborted by calling Abort.
|
|
||||||
type Abortable struct {
|
|
||||||
abortFunc []context.CancelFunc
|
|
||||||
abortLock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Abortable) Do(ctx context.Context, fn func(context.Context)) {
|
|
||||||
fn(a.newCancelCtx(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Abortable) Abort() {
|
|
||||||
a.abortLock.RLock()
|
|
||||||
defer a.abortLock.RUnlock()
|
|
||||||
|
|
||||||
for _, fn := range a.abortFunc {
|
|
||||||
fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Abortable) newCancelCtx(ctx context.Context) context.Context {
|
|
||||||
a.abortLock.Lock()
|
|
||||||
defer a.abortLock.Unlock()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
|
|
||||||
a.abortFunc = append(a.abortFunc, cancel)
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// RangeContext iterates over the given channel until the context is canceled or the
|
|
||||||
// channel is closed.
|
|
||||||
func RangeContext[T any](ctx context.Context, ch <-chan T, fn func(T)) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case v, ok := <-ch:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fn(v)
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForwardContext forwards all values from the src channel to the dst channel until the
|
|
||||||
// context is canceled or the src channel is closed.
|
|
||||||
func ForwardContext[T any](ctx context.Context, dst chan<- T, src <-chan T) {
|
|
||||||
RangeContext(ctx, src, func(v T) {
|
|
||||||
select {
|
|
||||||
case dst <- v:
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,233 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package async
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PanicHandler interface {
|
|
||||||
HandlePanic()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group is forked and improved version of "github.com/bradenaw/juniper/xsync.Group".
|
|
||||||
//
|
|
||||||
// It manages a group of goroutines. The main change to original is posibility
|
|
||||||
// to wait passed function to finish without canceling it's context and adding
|
|
||||||
// PanicHandler.
|
|
||||||
type Group struct {
|
|
||||||
baseCtx context.Context
|
|
||||||
ctx context.Context
|
|
||||||
jobCtx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
finish context.CancelFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
panicHandler PanicHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGroup returns a Group ready for use. The context passed to any of the f functions will be a
|
|
||||||
// descendant of ctx.
|
|
||||||
func NewGroup(ctx context.Context, panicHandler PanicHandler) *Group {
|
|
||||||
bgCtx, cancel := context.WithCancel(ctx)
|
|
||||||
jobCtx, finish := context.WithCancel(ctx)
|
|
||||||
return &Group{
|
|
||||||
baseCtx: ctx,
|
|
||||||
ctx: bgCtx,
|
|
||||||
jobCtx: jobCtx,
|
|
||||||
cancel: cancel,
|
|
||||||
finish: finish,
|
|
||||||
panicHandler: panicHandler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once calls f once from another goroutine.
|
|
||||||
func (g *Group) Once(f func(ctx context.Context)) {
|
|
||||||
g.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer g.handlePanic()
|
|
||||||
|
|
||||||
f(g.ctx)
|
|
||||||
g.wg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// jitterDuration returns a random duration in [d - jitter, d + jitter].
|
|
||||||
func jitterDuration(d time.Duration, jitter time.Duration) time.Duration {
|
|
||||||
return d + time.Duration(float64(jitter)*((rand.Float64()*2)-1)) //nolint:gosec
|
|
||||||
}
|
|
||||||
|
|
||||||
// Periodic spawns a goroutine that calls f once per interval +/- jitter.
|
|
||||||
func (g *Group) Periodic(
|
|
||||||
interval time.Duration,
|
|
||||||
jitter time.Duration,
|
|
||||||
f func(ctx context.Context),
|
|
||||||
) {
|
|
||||||
g.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer g.handlePanic()
|
|
||||||
|
|
||||||
defer g.wg.Done()
|
|
||||||
|
|
||||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
if g.ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-g.jobCtx.Done():
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Reset(jitterDuration(interval, jitter))
|
|
||||||
f(g.ctx)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger spawns a goroutine which calls f whenever the returned function is called. If f is
|
|
||||||
// already running when triggered, f will run again immediately when it finishes.
|
|
||||||
func (g *Group) Trigger(f func(ctx context.Context)) func() {
|
|
||||||
c := make(chan struct{}, 1)
|
|
||||||
g.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer g.handlePanic()
|
|
||||||
|
|
||||||
defer g.wg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
if g.ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-g.jobCtx.Done():
|
|
||||||
return
|
|
||||||
case <-c:
|
|
||||||
}
|
|
||||||
f(g.ctx)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
select {
|
|
||||||
case c <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeriodicOrTrigger spawns a goroutine which calls f whenever the returned function is called. If
|
|
||||||
// f is already running when triggered, f will run again immediately when it finishes. Also calls f
|
|
||||||
// when it has been interval+/-jitter since the last trigger.
|
|
||||||
func (g *Group) PeriodicOrTrigger(
|
|
||||||
interval time.Duration,
|
|
||||||
jitter time.Duration,
|
|
||||||
f func(ctx context.Context),
|
|
||||||
) func() {
|
|
||||||
c := make(chan struct{}, 1)
|
|
||||||
g.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer g.handlePanic()
|
|
||||||
|
|
||||||
defer g.wg.Done()
|
|
||||||
|
|
||||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
if g.ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-g.jobCtx.Done():
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
t.Reset(jitterDuration(interval, jitter))
|
|
||||||
case <-c:
|
|
||||||
if !t.Stop() {
|
|
||||||
<-t.C
|
|
||||||
}
|
|
||||||
t.Reset(jitterDuration(interval, jitter))
|
|
||||||
}
|
|
||||||
f(g.ctx)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
select {
|
|
||||||
case c <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Group) resetCtx() {
|
|
||||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
|
||||||
g.ctx, g.cancel = context.WithCancel(g.baseCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel is send to all of the spawn goroutines and ends periodic
|
|
||||||
// or trigger routines.
|
|
||||||
func (g *Group) Cancel() {
|
|
||||||
g.cancel()
|
|
||||||
g.finish()
|
|
||||||
g.resetCtx()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finish will ends all periodic or polls routines. It will let
|
|
||||||
// currently running functions to finish (cancel is not sent).
|
|
||||||
//
|
|
||||||
// It is not safe to call Wait concurrently with any other method on g.
|
|
||||||
func (g *Group) Finish() {
|
|
||||||
g.finish()
|
|
||||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelAndWait cancels the context passed to any of the spawned goroutines and waits for all spawned
|
|
||||||
// goroutines to exit.
|
|
||||||
//
|
|
||||||
// It is not safe to call Wait concurrently with any other method on g.
|
|
||||||
func (g *Group) CancelAndWait() {
|
|
||||||
g.finish()
|
|
||||||
g.cancel()
|
|
||||||
g.wg.Wait()
|
|
||||||
g.resetCtx()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitToFinish will ends all periodic or polls routines. It will wait for
|
|
||||||
// currently running functions to finish (cancel is not sent).
|
|
||||||
//
|
|
||||||
// It is not safe to call Wait concurrently with any other method on g.
|
|
||||||
func (g *Group) WaitToFinish() {
|
|
||||||
g.finish()
|
|
||||||
g.wg.Wait()
|
|
||||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Group) handlePanic() {
|
|
||||||
if g.panicHandler != nil {
|
|
||||||
g.panicHandler.HandlePanic()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -32,14 +33,14 @@ func defaultAPIOptions(
|
|||||||
version *semver.Version,
|
version *semver.Version,
|
||||||
cookieJar http.CookieJar,
|
cookieJar http.CookieJar,
|
||||||
transport http.RoundTripper,
|
transport http.RoundTripper,
|
||||||
poolSize int,
|
panicHandler async.PanicHandler,
|
||||||
) []proton.Option {
|
) []proton.Option {
|
||||||
return []proton.Option{
|
return []proton.Option{
|
||||||
proton.WithHostURL(apiURL),
|
proton.WithHostURL(apiURL),
|
||||||
proton.WithAppVersion(constants.AppVersion(version.Original())),
|
proton.WithAppVersion(constants.AppVersion(version.Original())),
|
||||||
proton.WithCookieJar(cookieJar),
|
proton.WithCookieJar(cookieJar),
|
||||||
proton.WithTransport(transport),
|
proton.WithTransport(transport),
|
||||||
proton.WithAttPoolSize(poolSize),
|
proton.WithLogger(logrus.WithField("pkg", "gpa/client")),
|
||||||
proton.WithLogger(logrus.StandardLogger()),
|
proton.WithPanicHandler(panicHandler),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//go:build !build_qa
|
//go:build !build_qa && !test_integration
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ func newAPIOptions(
|
|||||||
version *semver.Version,
|
version *semver.Version,
|
||||||
cookieJar http.CookieJar,
|
cookieJar http.CookieJar,
|
||||||
transport http.RoundTripper,
|
transport http.RoundTripper,
|
||||||
poolSize int,
|
panicHandler async.PanicHandler,
|
||||||
) []proton.Option {
|
) []proton.Option {
|
||||||
return defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
return defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -15,15 +15,17 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//go:build build_qa
|
//go:build build_qa || test_integration
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,9 +35,17 @@ func newAPIOptions(
|
|||||||
version *semver.Version,
|
version *semver.Version,
|
||||||
cookieJar http.CookieJar,
|
cookieJar http.CookieJar,
|
||||||
transport http.RoundTripper,
|
transport http.RoundTripper,
|
||||||
poolSize int,
|
panicHandler async.PanicHandler,
|
||||||
) []proton.Option {
|
) []proton.Option {
|
||||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
|
||||||
|
if allow := os.Getenv("BRIDGE_ALLOW_PROXY"); allow != "" {
|
||||||
|
transport = &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
|
||||||
|
|
||||||
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
|
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
|
||||||
opt = append(opt, proton.WithHostURL(host))
|
opt = append(opt, proton.WithHostURL(host))
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,31 +21,47 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon/async"
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gluon/watcher"
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/elastic/go-sysinfo/types"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var usernameChangeRegex = regexp.MustCompile(`^/Users/([^/]+)/`)
|
||||||
|
|
||||||
type Bridge struct {
|
type Bridge struct {
|
||||||
// vault holds bridge-specific data, such as preferences and known users (authorized or not).
|
// vault holds bridge-specific data, such as preferences and known users (authorized or not).
|
||||||
vault *vault.Vault
|
vault *vault.Vault
|
||||||
@ -57,23 +73,21 @@ type Bridge struct {
|
|||||||
// api manages user API clients.
|
// api manages user API clients.
|
||||||
api *proton.Manager
|
api *proton.Manager
|
||||||
proxyCtl ProxyController
|
proxyCtl ProxyController
|
||||||
identifier Identifier
|
identifier identifier.Identifier
|
||||||
|
|
||||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
// imapServer is the bridge's IMAP server.
|
// imapServer is the bridge's IMAP server.
|
||||||
imapServer *gluon.Server
|
imapEventCh chan imapEvents.Event
|
||||||
imapListener net.Listener
|
|
||||||
imapEventCh chan imapEvents.Event
|
|
||||||
|
|
||||||
// smtpServer is the bridge's SMTP server.
|
|
||||||
smtpServer *smtp.Server
|
|
||||||
smtpListener net.Listener
|
|
||||||
|
|
||||||
// updater is the bridge's updater.
|
// updater is the bridge's updater.
|
||||||
updater Updater
|
updater Updater
|
||||||
installCh chan installJob
|
installChLegacy chan installJobLegacy
|
||||||
|
installCh chan installJob
|
||||||
|
|
||||||
|
// heartbeat is the telemetry heartbeat for metrics.
|
||||||
|
heartbeat *heartBeatState
|
||||||
|
|
||||||
// curVersion is the current version of the bridge,
|
// curVersion is the current version of the bridge,
|
||||||
// newVersion is the version that was installed by the updater.
|
// newVersion is the version that was installed by the updater.
|
||||||
@ -81,6 +95,9 @@ type Bridge struct {
|
|||||||
newVersion *semver.Version
|
newVersion *semver.Version
|
||||||
newVersionLock safe.RWMutex
|
newVersionLock safe.RWMutex
|
||||||
|
|
||||||
|
// keychains is the utils that own usable keychains found in the OS.
|
||||||
|
keychains *keychain.List
|
||||||
|
|
||||||
// focusService is used to raise the bridge window when needed.
|
// focusService is used to raise the bridge window when needed.
|
||||||
focusService *focus.Service
|
focusService *focus.Service
|
||||||
|
|
||||||
@ -90,8 +107,8 @@ type Bridge struct {
|
|||||||
// locator is the bridge's locator.
|
// locator is the bridge's locator.
|
||||||
locator Locator
|
locator Locator
|
||||||
|
|
||||||
// crashHandler
|
// panicHandler
|
||||||
crashHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
|
|
||||||
// reporter
|
// reporter
|
||||||
reporter reporter.Reporter
|
reporter reporter.Reporter
|
||||||
@ -108,6 +125,12 @@ type Bridge struct {
|
|||||||
logIMAPServer bool
|
logIMAPServer bool
|
||||||
logSMTP bool
|
logSMTP bool
|
||||||
|
|
||||||
|
// These two variables keep track of the startup values for the two settings of the same name.
|
||||||
|
// They are updated in the vault on startup so that we're sure they're updated in case of kill/crash,
|
||||||
|
// but we need to keep their initial value for the current instance of bridge.
|
||||||
|
firstStart bool
|
||||||
|
lastVersion *semver.Version
|
||||||
|
|
||||||
// tasks manages the bridge's goroutines.
|
// tasks manages the bridge's goroutines.
|
||||||
tasks *async.Group
|
tasks *async.Group
|
||||||
|
|
||||||
@ -116,39 +139,60 @@ type Bridge struct {
|
|||||||
|
|
||||||
// goUpdate triggers a check/install of updates.
|
// goUpdate triggers a check/install of updates.
|
||||||
goUpdate func()
|
goUpdate func()
|
||||||
|
|
||||||
|
serverManager *imapsmtpserver.Service
|
||||||
|
syncService *syncservice.Service
|
||||||
|
|
||||||
|
// unleashService is responsible for polling the feature flags and caching
|
||||||
|
unleashService *unleash.Service
|
||||||
|
|
||||||
|
// observabilityService is responsible for handling calls to the observability system
|
||||||
|
observabilityService *observability.Service
|
||||||
|
|
||||||
|
// notificationStore is used for notification deduplication
|
||||||
|
notificationStore *notifications.Store
|
||||||
|
|
||||||
|
// getHostVersion primarily used for testing the update logic - it should return an OS version
|
||||||
|
getHostVersion func(host types.Host) string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
|
||||||
|
|
||||||
// New creates a new bridge.
|
// New creates a new bridge.
|
||||||
func New( //nolint:funlen
|
func New(
|
||||||
locator Locator, // the locator to provide paths to store data
|
locator Locator, // the locator to provide paths to store data
|
||||||
vault *vault.Vault, // the bridge's encrypted data store
|
vault *vault.Vault, // the bridge's encrypted data store
|
||||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||||
updater Updater, // the updater to fetch and install updates
|
updater Updater, // the updater to fetch and install updates
|
||||||
curVersion *semver.Version, // the current version of the bridge
|
curVersion *semver.Version, // the current version of the bridge
|
||||||
|
keychains *keychain.List, // usable keychains
|
||||||
|
|
||||||
apiURL string, // the URL of the API to use
|
apiURL string, // the URL of the API to use
|
||||||
cookieJar http.CookieJar, // the cookie jar to use
|
cookieJar http.CookieJar, // the cookie jar to use
|
||||||
identifier Identifier, // the identifier to keep track of the user agent
|
identifier identifier.Identifier, // the identifier to keep track of the user agent
|
||||||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||||
proxyCtl ProxyController, // the DoH controller
|
proxyCtl ProxyController, // the DoH controller
|
||||||
crashHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
heartBeatManager telemetry.HeartbeatManager,
|
||||||
|
|
||||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||||
logSMTP bool, // whether to log SMTP activity
|
logSMTP bool, // whether to log SMTP activity
|
||||||
) (*Bridge, <-chan events.Event, error) {
|
) (*Bridge, <-chan events.Event, error) {
|
||||||
// api is the user's API manager.
|
// api is the user's API manager.
|
||||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, vault.SyncAttPool())...)
|
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, panicHandler)...)
|
||||||
|
|
||||||
// tasks holds all the bridge's background tasks.
|
// tasks holds all the bridge's background tasks.
|
||||||
tasks := async.NewGroup(context.Background(), crashHandler)
|
tasks := async.NewGroup(context.Background(), panicHandler)
|
||||||
|
|
||||||
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
|
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
|
||||||
imapEventCh := make(chan imapEvents.Event)
|
imapEventCh := make(chan imapEvents.Event)
|
||||||
|
|
||||||
// bridge is the bridge.
|
// bridge is the bridge.
|
||||||
bridge, err := newBridge(
|
bridge, err := newBridge(
|
||||||
|
context.Background(),
|
||||||
tasks,
|
tasks,
|
||||||
imapEventCh,
|
imapEventCh,
|
||||||
|
|
||||||
@ -157,12 +201,15 @@ func New( //nolint:funlen
|
|||||||
autostarter,
|
autostarter,
|
||||||
updater,
|
updater,
|
||||||
curVersion,
|
curVersion,
|
||||||
crashHandler,
|
keychains,
|
||||||
|
panicHandler,
|
||||||
reporter,
|
reporter,
|
||||||
|
|
||||||
api,
|
api,
|
||||||
identifier,
|
identifier,
|
||||||
proxyCtl,
|
proxyCtl,
|
||||||
|
uidValidityGenerator,
|
||||||
|
heartBeatManager,
|
||||||
logIMAPClient, logIMAPServer, logSMTP,
|
logIMAPClient, logIMAPServer, logSMTP,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -177,23 +224,11 @@ func New( //nolint:funlen
|
|||||||
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start serving IMAP.
|
|
||||||
if err := bridge.serveIMAP(); err != nil {
|
|
||||||
logrus.WithError(err).Error("IMAP error")
|
|
||||||
bridge.PushError(ErrServeIMAP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start serving SMTP.
|
|
||||||
if err := bridge.serveSMTP(); err != nil {
|
|
||||||
logrus.WithError(err).Error("SMTP error")
|
|
||||||
bridge.PushError(ErrServeSMTP)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bridge, eventCh, nil
|
return bridge, eventCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
|
||||||
func newBridge(
|
func newBridge(
|
||||||
|
ctx context.Context,
|
||||||
tasks *async.Group,
|
tasks *async.Group,
|
||||||
imapEventCh chan imapEvents.Event,
|
imapEventCh chan imapEvents.Event,
|
||||||
|
|
||||||
@ -202,12 +237,15 @@ func newBridge(
|
|||||||
autostarter Autostarter,
|
autostarter Autostarter,
|
||||||
updater Updater,
|
updater Updater,
|
||||||
curVersion *semver.Version,
|
curVersion *semver.Version,
|
||||||
crashHandler async.PanicHandler,
|
keychains *keychain.List,
|
||||||
|
panicHandler async.PanicHandler,
|
||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
|
|
||||||
api *proton.Manager,
|
api *proton.Manager,
|
||||||
identifier Identifier,
|
identifier identifier.Identifier,
|
||||||
proxyCtl ProxyController,
|
proxyCtl ProxyController,
|
||||||
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
heartbeatManager telemetry.HeartbeatManager,
|
||||||
|
|
||||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||||
) (*Bridge, error) {
|
) (*Bridge, error) {
|
||||||
@ -216,30 +254,27 @@ func newBridge(
|
|||||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gluonDir, err := getGluonDir(vault)
|
firstStart := vault.GetFirstStart()
|
||||||
if err != nil {
|
if err := vault.SetFirstStart(false); err != nil {
|
||||||
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
return nil, fmt.Errorf("failed to save first start indicator: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
imapServer, err := newIMAPServer(
|
lastVersion := vault.GetLastVersion()
|
||||||
gluonDir,
|
if err := vault.SetLastVersion(curVersion); err != nil {
|
||||||
curVersion,
|
return nil, fmt.Errorf("failed to save last version indicator: %w", err)
|
||||||
tlsConfig,
|
|
||||||
reporter,
|
|
||||||
logIMAPClient,
|
|
||||||
logIMAPServer,
|
|
||||||
imapEventCh,
|
|
||||||
tasks,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
focusService, err := focus.NewService(curVersion)
|
identifier.SetClientString(vault.GetLastUserAgent())
|
||||||
|
|
||||||
|
focusService, err := focus.NewService(locator, curVersion, panicHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
||||||
|
|
||||||
|
observabilityService := observability.NewService(ctx, panicHandler)
|
||||||
|
|
||||||
bridge := &Bridge{
|
bridge := &Bridge{
|
||||||
vault: vault,
|
vault: vault,
|
||||||
|
|
||||||
@ -251,19 +286,23 @@ func newBridge(
|
|||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
|
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
imapServer: imapServer,
|
|
||||||
imapEventCh: imapEventCh,
|
imapEventCh: imapEventCh,
|
||||||
|
|
||||||
updater: updater,
|
updater: updater,
|
||||||
installCh: make(chan installJob),
|
installChLegacy: make(chan installJobLegacy),
|
||||||
|
installCh: make(chan installJob),
|
||||||
|
|
||||||
curVersion: curVersion,
|
curVersion: curVersion,
|
||||||
newVersion: curVersion,
|
newVersion: curVersion,
|
||||||
newVersionLock: safe.NewRWMutex(),
|
newVersionLock: safe.NewRWMutex(),
|
||||||
|
|
||||||
crashHandler: crashHandler,
|
keychains: keychains,
|
||||||
|
|
||||||
|
panicHandler: panicHandler,
|
||||||
reporter: reporter,
|
reporter: reporter,
|
||||||
|
|
||||||
|
heartbeat: newHeartBeatState(ctx, panicHandler),
|
||||||
|
|
||||||
focusService: focusService,
|
focusService: focusService,
|
||||||
autostarter: autostarter,
|
autostarter: autostarter,
|
||||||
locator: locator,
|
locator: locator,
|
||||||
@ -272,15 +311,54 @@ func newBridge(
|
|||||||
logIMAPServer: logIMAPServer,
|
logIMAPServer: logIMAPServer,
|
||||||
logSMTP: logSMTP,
|
logSMTP: logSMTP,
|
||||||
|
|
||||||
tasks: tasks,
|
firstStart: firstStart,
|
||||||
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
|
tasks: tasks,
|
||||||
|
syncService: syncservice.NewService(panicHandler, observabilityService),
|
||||||
|
|
||||||
|
unleashService: unleashService,
|
||||||
|
|
||||||
|
observabilityService: observabilityService,
|
||||||
|
|
||||||
|
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||||
|
|
||||||
|
getHostVersion: func(host types.Host) string { return host.Info().OS.Version },
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
||||||
|
&bridgeSMTPSettings{b: bridge},
|
||||||
|
&bridgeIMAPSettings{b: bridge},
|
||||||
|
&bridgeEventPublisher{b: bridge},
|
||||||
|
panicHandler,
|
||||||
|
reporter,
|
||||||
|
uidValidityGenerator,
|
||||||
|
&bridgeIMAPSMTPTelemetry{b: bridge},
|
||||||
|
observabilityService,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check whether username has changed and correct (macOS only)
|
||||||
|
bridge.verifyUsernameChange()
|
||||||
|
|
||||||
|
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if heartbeatManager == nil {
|
||||||
|
bridge.heartbeat.init(bridge, bridge)
|
||||||
|
} else {
|
||||||
|
bridge.heartbeat.init(bridge, heartbeatManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.syncService.Run()
|
||||||
|
|
||||||
|
bridge.unleashService.Run()
|
||||||
|
|
||||||
|
bridge.observabilityService.Run(bridge)
|
||||||
|
|
||||||
return bridge, nil
|
return bridge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
|
||||||
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||||
// Enable or disable the proxy at startup.
|
// Enable or disable the proxy at startup.
|
||||||
if bridge.vault.GetProxyAllowed() {
|
if bridge.vault.GetProxyAllowed() {
|
||||||
@ -291,7 +369,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
|
|
||||||
// Handle connection up/down events.
|
// Handle connection up/down events.
|
||||||
bridge.api.AddStatusObserver(func(status proton.Status) {
|
bridge.api.AddStatusObserver(func(status proton.Status) {
|
||||||
logrus.Info("API status changed: ", status)
|
logPkg.Info("API status changed: ", status)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case status == proton.StatusUp:
|
case status == proton.StatusUp:
|
||||||
@ -306,7 +384,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
|
|
||||||
// If any call returns a bad version code, we need to update.
|
// If any call returns a bad version code, we need to update.
|
||||||
bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() {
|
bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() {
|
||||||
logrus.Warn("App version is bad")
|
logPkg.Warn("App version is bad")
|
||||||
bridge.publish(events.UpdateForced{})
|
bridge.publish(events.UpdateForced{})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -319,7 +397,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
// Log all manager API requests (client requests are logged separately).
|
// Log all manager API requests (client requests are logged separately).
|
||||||
bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error {
|
bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error {
|
||||||
if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok {
|
if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok {
|
||||||
logrus.Infof("[MANAGER] %v: %v %v", r.Status(), r.Request.Method, r.Request.URL)
|
logrus.WithField("pkg", "gpa/manager").Infof("%v: %v %v", r.Status(), r.Request.Method, r.Request.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -328,7 +406,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
// Publish a TLS issue event if a TLS issue is encountered.
|
// Publish a TLS issue event if a TLS issue is encountered.
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) {
|
async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) {
|
||||||
logrus.Warn("TLS issue encountered")
|
logPkg.Warn("TLS issue encountered")
|
||||||
bridge.publish(events.TLSIssue{})
|
bridge.publish(events.TLSIssue{})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -336,7 +414,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
// Publish a raise event if the focus service is called.
|
// Publish a raise event if the focus service is called.
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) {
|
async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) {
|
||||||
logrus.Info("Focus service requested raise")
|
logPkg.Info("Focus service requested raise")
|
||||||
bridge.publish(events.Raise{})
|
bridge.publish(events.Raise{})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -344,37 +422,68 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
// Handle any IMAP events that are forwarded to the bridge from gluon.
|
// Handle any IMAP events that are forwarded to the bridge from gluon.
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) {
|
async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) {
|
||||||
logrus.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event")
|
logPkg.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event")
|
||||||
bridge.handleIMAPEvent(event)
|
bridge.handleIMAPEvent(event)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Attempt to lazy load users when triggered.
|
// Attempt to load users from the vault when triggered.
|
||||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||||
logrus.Info("Loading users")
|
|
||||||
|
|
||||||
if err := bridge.loadUsers(ctx); err != nil {
|
if err := bridge.loadUsers(ctx); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to load users")
|
logPkg.WithError(err).Error("Failed to load users")
|
||||||
} else {
|
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
|
||||||
bridge.publish(events.AllUsersLoaded{})
|
sentry.ReportError(bridge.reporter, "Failed to load users", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.publish(events.AllUsersLoaded{})
|
||||||
})
|
})
|
||||||
defer bridge.goLoad()
|
defer bridge.goLoad()
|
||||||
|
|
||||||
// Check for updates when triggered.
|
// Check for updates when triggered.
|
||||||
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
|
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
|
||||||
logrus.Info("Checking for updates")
|
logPkg.Info("Checking for updates")
|
||||||
|
var versionLegacy updater.VersionInfoLegacy
|
||||||
|
var version updater.VersionInfo
|
||||||
|
var err error
|
||||||
|
|
||||||
|
useOldUpdateLogic := bridge.GetFeatureFlagValue(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
if useOldUpdateLogic {
|
||||||
|
versionLegacy, err = bridge.updater.GetVersionInfoLegacy(ctx, bridge.api, bridge.vault.GetUpdateChannel())
|
||||||
|
} else {
|
||||||
|
version, err = bridge.updater.GetVersionInfo(ctx, bridge.api)
|
||||||
|
}
|
||||||
|
|
||||||
version, err := bridge.updater.GetVersionInfo(ctx, bridge.api, bridge.vault.GetUpdateChannel())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bridge.publish(events.UpdateCheckFailed{Error: err})
|
bridge.publish(events.UpdateCheckFailed{Error: err})
|
||||||
|
if errors.Is(err, updater.ErrVersionFileDownloadOrVerify) {
|
||||||
|
logPkg.WithError(err).Error("Cannot download or verify the version file")
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Cannot download or verify the version file",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
logPkg.WithError(reporterErr).Error("Failed to report version file check error")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
bridge.handleUpdate(version)
|
if useOldUpdateLogic {
|
||||||
|
bridge.handleUpdateLegacy(versionLegacy)
|
||||||
|
} else {
|
||||||
|
bridge.handleUpdate(version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
defer bridge.goUpdate()
|
defer bridge.goUpdate()
|
||||||
|
|
||||||
// Install updates when available.
|
// Install updates when available - based on old update logic
|
||||||
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
|
async.RangeContext(ctx, bridge.installChLegacy, func(job installJobLegacy) {
|
||||||
|
bridge.installUpdateLegacy(ctx, job)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Install updates when available - based on new update logic
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
async.RangeContext(ctx, bridge.installCh, func(job installJob) {
|
async.RangeContext(ctx, bridge.installCh, func(job installJob) {
|
||||||
bridge.installUpdate(ctx, job)
|
bridge.installUpdate(ctx, job)
|
||||||
@ -401,31 +510,37 @@ func (bridge *Bridge) GetErrors() []error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) Close(ctx context.Context) {
|
func (bridge *Bridge) Close(ctx context.Context) {
|
||||||
logrus.Info("Closing bridge")
|
logPkg.Info("Closing bridge")
|
||||||
|
|
||||||
// Close the IMAP server.
|
// Stop observability service
|
||||||
if err := bridge.closeIMAP(ctx); err != nil {
|
bridge.observabilityService.Stop()
|
||||||
logrus.WithError(err).Error("Failed to close IMAP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the SMTP server.
|
// Stop heart beat before closing users.
|
||||||
if err := bridge.closeSMTP(); err != nil {
|
bridge.heartbeat.stop()
|
||||||
logrus.WithError(err).Error("Failed to close SMTP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all users.
|
// Close all users.
|
||||||
safe.RLock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.Close()
|
user.Close()
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
|
// Close the servers
|
||||||
|
if err := bridge.serverManager.CloseServers(ctx); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed to close servers")
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.syncService.Close()
|
||||||
|
|
||||||
// Stop all ongoing tasks.
|
// Stop all ongoing tasks.
|
||||||
bridge.tasks.CancelAndWait()
|
bridge.tasks.CancelAndWait()
|
||||||
|
|
||||||
// Close the focus service.
|
// Close the focus service.
|
||||||
bridge.focusService.Close()
|
bridge.focusService.Close()
|
||||||
|
|
||||||
|
// Close the unleash service.
|
||||||
|
bridge.unleashService.Close()
|
||||||
|
|
||||||
// Close the watchers.
|
// Close the watchers.
|
||||||
bridge.watchersLock.Lock()
|
bridge.watchersLock.Lock()
|
||||||
defer bridge.watchersLock.Unlock()
|
defer bridge.watchersLock.Unlock()
|
||||||
@ -435,23 +550,18 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bridge.watchers = nil
|
bridge.watchers = nil
|
||||||
|
|
||||||
// Save the last version of bridge that was run.
|
|
||||||
if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to save last version")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) publish(event events.Event) {
|
func (bridge *Bridge) publish(event events.Event) {
|
||||||
bridge.watchersLock.RLock()
|
bridge.watchersLock.RLock()
|
||||||
defer bridge.watchersLock.RUnlock()
|
defer bridge.watchersLock.RUnlock()
|
||||||
|
|
||||||
logrus.WithField("event", event).Debug("Publishing event")
|
logPkg.WithField("event", event).Debug("Publishing event")
|
||||||
|
|
||||||
for _, watcher := range bridge.watchers {
|
for _, watcher := range bridge.watchers {
|
||||||
if watcher.IsWatching(event) {
|
if watcher.IsWatching(event) {
|
||||||
if ok := watcher.Send(event); !ok {
|
if ok := watcher.Send(event); !ok {
|
||||||
logrus.WithField("event", event).Warn("Failed to send event to watcher")
|
logPkg.WithField("event", event).Warn("Failed to send event to watcher")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -461,7 +571,7 @@ func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events
|
|||||||
bridge.watchersLock.Lock()
|
bridge.watchersLock.Lock()
|
||||||
defer bridge.watchersLock.Unlock()
|
defer bridge.watchersLock.Unlock()
|
||||||
|
|
||||||
watcher := watcher.New(ofType...)
|
watcher := watcher.New(bridge.panicHandler, ofType...)
|
||||||
|
|
||||||
bridge.watchers = append(bridge.watchers, watcher)
|
bridge.watchers = append(bridge.watchers, watcher)
|
||||||
|
|
||||||
@ -483,26 +593,14 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
|||||||
watcher.Close()
|
watcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
func (bridge *Bridge) onStatusUp(_ context.Context) {
|
||||||
logrus.Info("Handling API status up")
|
logPkg.Info("Handling API status up")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusUp(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
bridge.goLoad()
|
bridge.goLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||||
logrus.Info("Handling API status down")
|
logPkg.Info("Handling API status down")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusDown(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
||||||
select {
|
select {
|
||||||
@ -510,10 +608,10 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
case <-time.After(backoff):
|
case <-time.After(backoff):
|
||||||
logrus.Info("Pinging API")
|
logPkg.Info("Pinging API")
|
||||||
|
|
||||||
if err := bridge.api.Ping(ctx); err != nil {
|
if err := bridge.api.Ping(ctx); err != nil {
|
||||||
logrus.WithError(err).Warn("Ping failed, API is still unreachable")
|
logPkg.WithError(err).Warn("Ping failed, API is still unreachable")
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -521,8 +619,51 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) Repair() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
userIDs := bridge.GetUserIDs()
|
||||||
|
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
logPkg.Info("Initiating repair for userID:", userID)
|
||||||
|
|
||||||
|
userInfo, err := bridge.GetUserInfo(userID)
|
||||||
|
if err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed getting user info for repair; ID:", userID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfo.State != Connected {
|
||||||
|
logPkg.Info("User is not connected. Repair will be executed on following successful log in.", userID)
|
||||||
|
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
|
||||||
|
if err := user.SetShouldSync(true); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed setting vault should sync for user:", userID)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Unable to get user vault when scheduling repair:", userID)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bridgeUser, ok := bridge.users[userID]
|
||||||
|
if !ok {
|
||||||
|
logPkg.Info("UserID does not exist in bridge user map", userID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(userID string) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err = bridgeUser.TriggerRepair(); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed re-syncing IMAP for userID", userID)
|
||||||
|
}
|
||||||
|
}(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
|
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -533,28 +674,113 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
|
func (bridge *Bridge) HasAPIConnection() bool {
|
||||||
if useTLS {
|
return bridge.api.GetStatus() == proton.StatusUp
|
||||||
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlsListener, nil
|
// verifyUsernameChange - works only on macOS
|
||||||
|
// it attempts to check whether a username change has taken place by comparing the gluon DB path (which is static and provided by bridge)
|
||||||
|
// to the gluon Cache path - which can be modified by the user and is stored in the vault;
|
||||||
|
// if a username discrepancy is detected, and the cache folder does not exist with the "old" username
|
||||||
|
// then we verify whether the gluon cache exists using the "new" username (provided by the DB path in this case)
|
||||||
|
// if so we modify the cache directory in the user vault.
|
||||||
|
func (bridge *Bridge) verifyUsernameChange() {
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
|
gluonDBPath, err := bridge.GetGluonDataDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logPkg.WithError(err).Error("Failed to get gluon db path")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return netListener, nil
|
gluonCachePath := bridge.GetGluonCacheDir()
|
||||||
}
|
// If the cache folder exists even on another user account or is in `/Users/Shared` we would still be able to access it
|
||||||
|
// though it depends on the permissions; this is an edge-case.
|
||||||
func min(a, b time.Duration) time.Duration {
|
if _, err := os.Stat(gluonCachePath); err == nil {
|
||||||
if a < b {
|
return
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return b
|
newCacheDir := GetUpdatedCachePath(gluonDBPath, gluonCachePath)
|
||||||
|
if newCacheDir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(newCacheDir); err == nil {
|
||||||
|
logPkg.Info("Username change detected. Trying to restore gluon cache directory")
|
||||||
|
if err = bridge.vault.SetGluonDir(newCacheDir); err != nil {
|
||||||
|
logPkg.WithError(err).Error("Failed to restore gluon cache directory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logPkg.Info("Successfully restored gluon cache directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string {
|
||||||
|
// If gluon cache is moved to an external drive; regex find will fail; as is expected
|
||||||
|
cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath)
|
||||||
|
if cachePathMatches == nil || len(cachePathMatches) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheUsername := cachePathMatches[1]
|
||||||
|
dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath)
|
||||||
|
if dbPathMatches == nil || len(dbPathMatches) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUsername := dbPathMatches[1]
|
||||||
|
if cacheUsername == dbUsername {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Replace(gluonCachePath, "/Users/"+cacheUsername+"/", "/Users/"+dbUsername+"/", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
|
||||||
|
return bridge.unleashService.GetFlagValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
|
||||||
|
bridge.observabilityService.AddMetrics(metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||||
|
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
|
||||||
|
bridge.observabilityService.ModifyHeartbeatInterval(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) ReportMessageWithContext(message string, messageCtx reporter.Context) {
|
||||||
|
if err := bridge.reporter.ReportMessageWithContext(message, messageCtx); err != nil {
|
||||||
|
logPkg.WithFields(logrus.Fields{
|
||||||
|
"err": err,
|
||||||
|
"sentryMessage": message,
|
||||||
|
"messageCtx": messageCtx,
|
||||||
|
}).Info("Error occurred when sending Report to Sentry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsers is only used for testing purposes.
|
||||||
|
func (bridge *Bridge) GetUsers() map[string]*user.User {
|
||||||
|
return bridge.users
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCurrentVersionTest - sets the current version of bridge; should only be used for tests.
|
||||||
|
func (bridge *Bridge) SetCurrentVersionTest(version *semver.Version) {
|
||||||
|
bridge.curVersion = version
|
||||||
|
bridge.newVersion = version
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHostVersionGetterTest - sets the OS version helper func; only used for testing.
|
||||||
|
func (bridge *Bridge) SetHostVersionGetterTest(fn func(host types.Host) string) {
|
||||||
|
bridge.getHostVersion = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRolloutPercentageTest - sets the rollout percentage; should only be used for testing.
|
||||||
|
func (bridge *Bridge) SetRolloutPercentageTest(rollout float64) error {
|
||||||
|
return bridge.vault.SetUpdateRollout(rollout)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,31 +21,43 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/tests"
|
"github.com/ProtonMail/proton-bridge/v3/tests"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
imapid "github.com/emersion/go-imap-id"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -65,7 +77,7 @@ func init() {
|
|||||||
|
|
||||||
func TestBridge_ConnStatus(t *testing.T) {
|
func TestBridge_ConnStatus(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Get a stream of connection status events.
|
// Get a stream of connection status events.
|
||||||
eventCh, done := bridge.GetEvents(events.ConnStatusUp{}, events.ConnStatusDown{})
|
eventCh, done := bridge.GetEvents(events.ConnStatusUp{}, events.ConnStatusDown{})
|
||||||
defer done()
|
defer done()
|
||||||
@ -114,13 +126,16 @@ func TestBridge_TLSIssue(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_Focus(t *testing.T) {
|
func TestBridge_Focus(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Get a stream of TLS issue events.
|
// Get a stream of TLS issue events.
|
||||||
raiseCh, done := bridge.GetEvents(events.Raise{})
|
raiseCh, done := bridge.GetEvents(events.Raise{})
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
|
settingsFolder, err := locator.ProvideSettingsPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Simulate a focus event.
|
// Simulate a focus event.
|
||||||
focus.TryRaise()
|
focus.TryRaise(settingsFolder)
|
||||||
|
|
||||||
// Wait for the event.
|
// Wait for the event.
|
||||||
require.IsType(t, events.Raise{}, <-raiseCh)
|
require.IsType(t, events.Raise{}, <-raiseCh)
|
||||||
@ -142,7 +157,7 @@ func TestBridge_UserAgent(t *testing.T) {
|
|||||||
calls = append(calls, call)
|
calls = append(calls, call)
|
||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Set the platform to something other than the default.
|
// Set the platform to something other than the default.
|
||||||
bridge.SetCurrentPlatform("platform")
|
bridge.SetCurrentPlatform("platform")
|
||||||
|
|
||||||
@ -162,6 +177,174 @@ func TestBridge_UserAgent(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_UserAgent_Persistence(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
otherPassword := []byte("bar")
|
||||||
|
otherUser := "foo"
|
||||||
|
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
currentUserAgent := b.GetCurrentUserAgent()
|
||||||
|
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||||
|
|
||||||
|
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
||||||
|
|
||||||
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = imapClient.Logout() }()
|
||||||
|
|
||||||
|
idClient := imapid.NewClient(imapClient)
|
||||||
|
|
||||||
|
// Set IMAP ID before Login to have the value capture in the Login API Call.
|
||||||
|
_, err = idClient.ID(imapid.ID{
|
||||||
|
imapid.FieldName: "MyFancyClient",
|
||||||
|
imapid.FieldVersion: "0.1.2",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Login the user.
|
||||||
|
_, err = b.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Assert that the user agent then contains the platform.
|
||||||
|
require.Contains(t, b.GetCurrentUserAgent(), "MyFancyClient/0.1.2")
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
currentUserAgent := bridge.GetCurrentUserAgent()
|
||||||
|
require.Contains(t, currentUserAgent, "MyFancyClient/0.1.2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_UserAgentFromUnknownClient(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
otherPassword := []byte("bar")
|
||||||
|
otherUser := "foo"
|
||||||
|
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
currentUserAgent := b.GetCurrentUserAgent()
|
||||||
|
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||||
|
|
||||||
|
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = imapClient.Logout() }()
|
||||||
|
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
|
||||||
|
currentUserAgent = b.GetCurrentUserAgent()
|
||||||
|
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
otherPassword := []byte("bar")
|
||||||
|
otherUser := "foo"
|
||||||
|
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
currentUserAgent := b.GetCurrentUserAgent()
|
||||||
|
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||||
|
|
||||||
|
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||||
|
info.Addresses[0],
|
||||||
|
string(info.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
currentUserAgent = b.GetCurrentUserAgent()
|
||||||
|
|
||||||
|
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
|
||||||
|
}, time.Minute, 5*time.Second)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_UserAgentFromIMAPID(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
var (
|
||||||
|
calls []server.Call
|
||||||
|
lock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
s.AddCallWatcher(func(call server.Call) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
calls = append(calls, call)
|
||||||
|
})
|
||||||
|
|
||||||
|
otherPassword := []byte("bar")
|
||||||
|
otherUser := "foo"
|
||||||
|
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
||||||
|
|
||||||
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = imapClient.Logout() }()
|
||||||
|
|
||||||
|
idClient := imapid.NewClient(imapClient)
|
||||||
|
|
||||||
|
// Set IMAP ID before Login to have the value capture in the Login API Call.
|
||||||
|
_, err = idClient.ID(imapid.ID{
|
||||||
|
imapid.FieldName: "MyFancyClient",
|
||||||
|
imapid.FieldVersion: "0.1.2",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Login the user.
|
||||||
|
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
userAgent := calls[len(calls)-1].RequestHeader.Get("User-Agent")
|
||||||
|
|
||||||
|
// Assert that the user agent was sent to the API.
|
||||||
|
require.Contains(t, userAgent, b.GetCurrentUserAgent())
|
||||||
|
require.Contains(t, userAgent, "MyFancyClient/0.1.2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestBridge_Cookies(t *testing.T) {
|
func TestBridge_Cookies(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
var (
|
var (
|
||||||
@ -183,13 +366,13 @@ func TestBridge_Cookies(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Start bridge and add a user so that API assigns us a session ID via cookie.
|
// Start bridge and add a user so that API assigns us a session ID via cookie.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start bridge again and check that it uses the same session ID.
|
// Start bridge again and check that it uses the same session ID.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -201,9 +384,14 @@ func TestBridge_Cookies(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_CheckUpdate(t *testing.T) {
|
func TestBridge_CheckUpdate_Legacy(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Disable autoupdate for this test.
|
// Disable autoupdate for this test.
|
||||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||||
|
|
||||||
@ -218,7 +406,7 @@ func TestBridge_CheckUpdate(t *testing.T) {
|
|||||||
require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh)
|
require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh)
|
||||||
|
|
||||||
// Simulate a new version being available.
|
// Simulate a new version being available.
|
||||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
|
||||||
|
|
||||||
// Get a stream of update available events.
|
// Get a stream of update available events.
|
||||||
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||||
@ -229,7 +417,7 @@ func TestBridge_CheckUpdate(t *testing.T) {
|
|||||||
|
|
||||||
// We should receive an event indicating that an update is available.
|
// We should receive an event indicating that an update is available.
|
||||||
require.Equal(t, events.UpdateAvailable{
|
require.Equal(t, events.UpdateAvailable{
|
||||||
Version: updater.VersionInfo{
|
VersionLegacy: updater.VersionInfoLegacy{
|
||||||
Version: v2_4_0,
|
Version: v2_4_0,
|
||||||
MinAuto: v2_3_0,
|
MinAuto: v2_3_0,
|
||||||
RolloutProportion: 1.0,
|
RolloutProportion: 1.0,
|
||||||
@ -241,25 +429,30 @@ func TestBridge_CheckUpdate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_AutoUpdate(t *testing.T) {
|
func TestBridge_AutoUpdate_Legacy(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Enable autoupdate for this test.
|
// Enable autoupdate for this test.
|
||||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
require.NoError(t, b.SetAutoUpdate(true))
|
||||||
|
|
||||||
// Get a stream of update events.
|
// Get a stream of update events.
|
||||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
updateCh, done := b.GetEvents(events.UpdateInstalled{})
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
// Simulate a new version being available.
|
// Simulate a new version being available.
|
||||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
|
||||||
|
|
||||||
// Check for updates.
|
// Check for updates.
|
||||||
bridge.CheckForUpdates()
|
b.CheckForUpdates()
|
||||||
|
|
||||||
// We should receive an event indicating that the update was silently installed.
|
// We should receive an event indicating that the update was silently installed.
|
||||||
require.Equal(t, events.UpdateInstalled{
|
require.Equal(t, events.UpdateInstalled{
|
||||||
Version: updater.VersionInfo{
|
VersionLegacy: updater.VersionInfoLegacy{
|
||||||
Version: v2_4_0,
|
Version: v2_4_0,
|
||||||
MinAuto: v2_3_0,
|
MinAuto: v2_3_0,
|
||||||
RolloutProportion: 1.0,
|
RolloutProportion: 1.0,
|
||||||
@ -270,9 +463,14 @@ func TestBridge_AutoUpdate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ManualUpdate(t *testing.T) {
|
func TestBridge_ManualUpdate_Legacy(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Disable autoupdate for this test.
|
// Disable autoupdate for this test.
|
||||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||||
|
|
||||||
@ -281,14 +479,14 @@ func TestBridge_ManualUpdate(t *testing.T) {
|
|||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
// Simulate a new version being available, but it's too new for us.
|
// Simulate a new version being available, but it's too new for us.
|
||||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_4_0)
|
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_4_0)
|
||||||
|
|
||||||
// Check for updates.
|
// Check for updates.
|
||||||
bridge.CheckForUpdates()
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
// We should receive an event indicating an update is available, but we can't install it.
|
// We should receive an event indicating an update is available, but we can't install it.
|
||||||
require.Equal(t, events.UpdateAvailable{
|
require.Equal(t, events.UpdateAvailable{
|
||||||
Version: updater.VersionInfo{
|
VersionLegacy: updater.VersionInfoLegacy{
|
||||||
Version: v2_4_0,
|
Version: v2_4_0,
|
||||||
MinAuto: v2_4_0,
|
MinAuto: v2_4_0,
|
||||||
RolloutProportion: 1.0,
|
RolloutProportion: 1.0,
|
||||||
@ -302,7 +500,12 @@ func TestBridge_ManualUpdate(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_ForceUpdate(t *testing.T) {
|
func TestBridge_ForceUpdate(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Wait for FF poll.
|
||||||
|
time.Sleep(600 * time.Millisecond)
|
||||||
// Get a stream of update events.
|
// Get a stream of update events.
|
||||||
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
||||||
defer done()
|
defer done()
|
||||||
@ -325,7 +528,7 @@ func TestBridge_BadVaultKey(t *testing.T) {
|
|||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
// Login a user.
|
// Login a user.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
newUserID, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
newUserID, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -333,27 +536,27 @@ func TestBridge_BadVaultKey(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Start bridge with the correct vault key -- it should load the users correctly.
|
// Start bridge with the correct vault key -- it should load the users correctly.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs())
|
require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs())
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start bridge with a bad vault key, the vault will be wiped and bridge will show no users.
|
// Start bridge with a bad vault key, the vault will be wiped and bridge will show no users.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Empty(t, bridge.GetUserIDs())
|
require.Empty(t, bridge.GetUserIDs())
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start bridge with a nil vault key, the vault will be wiped and bridge will show no users.
|
// Start bridge with a nil vault key, the vault will be wiped and bridge will show no users.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Empty(t, bridge.GetUserIDs())
|
require.Empty(t, bridge.GetUserIDs())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_MissingGluonDir(t *testing.T) {
|
func TestBridge_MissingGluonStore(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
var gluonDir string
|
var gluonDir string
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -361,14 +564,37 @@ func TestBridge_MissingGluonDir(t *testing.T) {
|
|||||||
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
||||||
|
|
||||||
// Get the gluon dir.
|
// Get the gluon dir.
|
||||||
gluonDir = bridge.GetGluonDir()
|
gluonDir = bridge.GetGluonCacheDir()
|
||||||
})
|
})
|
||||||
|
|
||||||
// The user removes the gluon dir while bridge is not running.
|
// The user removes the gluon dir while bridge is not running.
|
||||||
require.NoError(t, os.RemoveAll(gluonDir))
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
// Bridge starts but can't find the gluon dir; there should be no error.
|
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_MissingGluonDatabase(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
var gluonDir string
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the gluon dir.
|
||||||
|
gluonDir, err = bridge.GetGluonDataDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The user removes the gluon dir while bridge is not running.
|
||||||
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
|
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -382,7 +608,11 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
|||||||
)
|
)
|
||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Watch for sync finished event.
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
// Create a user which will have an address without keys.
|
// Create a user which will have an address without keys.
|
||||||
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -403,10 +633,6 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
|||||||
// Remove the address keys.
|
// Remove the address keys.
|
||||||
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
|
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
|
||||||
|
|
||||||
// Watch for sync finished event.
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
// We should be able to log the user in.
|
// We should be able to log the user in.
|
||||||
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -456,47 +682,170 @@ func TestBridge_FactoryReset(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ChangeCacheDirectoryFailsBetweenDifferentVolumes(t *testing.T) {
|
func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Test only necessary on windows")
|
|
||||||
}
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Change directory
|
configDir, err := b.GetGluonDataDir()
|
||||||
err := bridge.SetGluonDir(ctx, "XX:\\")
|
require.NoError(t, err)
|
||||||
require.Error(t, err)
|
|
||||||
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_LoginFailed(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Error(t, imapClient.Login("badUser", "badPass"))
|
||||||
|
require.Equal(t, "badUser", (<-failCh).Username)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
newCacheDir := t.TempDir()
|
newCacheDir := t.TempDir()
|
||||||
currentCacheDir := bridge.GetGluonDir()
|
currentCacheDir := b.GetGluonCacheDir()
|
||||||
|
configDir, err := b.GetGluonDataDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
// The user is now connected.
|
// The user is now connected.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, b.GetUserIDs())
|
||||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
require.Equal(t, []string{userID}, getConnectedUserIDs(t, b))
|
||||||
|
|
||||||
// Change directory
|
// Change directory
|
||||||
err = bridge.SetGluonDir(ctx, newCacheDir)
|
err = b.SetGluonDir(ctx, newCacheDir)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.ReadDir(currentCacheDir)
|
// Old store should no more exists.
|
||||||
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
|
// Database should not have changed.
|
||||||
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
require.Equal(t, newCacheDir, bridge.GetGluonDir())
|
// New path should have Gluon sub-folder.
|
||||||
|
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||||
|
// And store should be inside it.
|
||||||
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
// We should be able to fetch.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
status, err := client.Select(`Folders/folder`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(10), status.Messages)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a second address for the user.
|
||||||
|
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Log the user in with its first address.
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
|
// We should see 10 messages in the inbox.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
status, err := client.Select(`Inbox`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(10), status.Messages)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make the second address the primary one.
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.OrderAddresses(ctx, proton.OrderAddressesReq{AddressIDs: []string{aliasID, addrID}}))
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// We should still see 10 messages in the inbox.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := client.Select(`Inbox`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 10
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// withEnv creates the full test environment and runs the tests.
|
// withEnv creates the full test environment and runs the tests.
|
||||||
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
|
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
|
||||||
|
opt := goleak.IgnoreCurrent()
|
||||||
|
defer goleak.VerifyNone(t, opt)
|
||||||
|
|
||||||
server := server.New(opts...)
|
server := server.New(opts...)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
@ -522,20 +871,29 @@ func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.N
|
|||||||
tests(ctx, server, netCtl, locations, vaultKey)
|
tests(ctx, server, netCtl, locations, vaultKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withMocks creates the mock objects used in the tests.
|
||||||
|
func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
|
||||||
|
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
||||||
|
defer mocks.Close()
|
||||||
|
|
||||||
|
tests(mocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needs to be global to survive bridge shutdown/startup in unit tests as they happen to fast.
|
||||||
|
var testUIDValidityGenerator = imap.DefaultEpochUIDValidityGenerator()
|
||||||
|
|
||||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||||
func withBridge(
|
func withBridgeNoMocks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
|
mocks *bridge.Mocks,
|
||||||
apiURL string,
|
apiURL string,
|
||||||
netCtl *proton.NetCtl,
|
netCtl *proton.NetCtl,
|
||||||
locator bridge.Locator,
|
locator bridge.Locator,
|
||||||
vaultKey []byte,
|
vaultKey []byte,
|
||||||
tests func(*bridge.Bridge, *bridge.Mocks),
|
tests func(*bridge.Bridge),
|
||||||
|
waitOnServers bool,
|
||||||
) {
|
) {
|
||||||
// Create the mock objects used in the tests.
|
|
||||||
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
|
||||||
defer mocks.Close()
|
|
||||||
|
|
||||||
// Bridge will disable the proxy by default at startup.
|
// Bridge will disable the proxy by default at startup.
|
||||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||||
|
|
||||||
@ -544,7 +902,7 @@ func withBridge(
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create the vault.
|
// Create the vault.
|
||||||
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey)
|
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey, async.NoopPanicHandler{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create a new cookie jar.
|
// Create a new cookie jar.
|
||||||
@ -560,6 +918,7 @@ func withBridge(
|
|||||||
mocks.Autostarter,
|
mocks.Autostarter,
|
||||||
mocks.Updater,
|
mocks.Updater,
|
||||||
v2_3_0,
|
v2_3_0,
|
||||||
|
keychain.NewTestKeychainsList(),
|
||||||
|
|
||||||
// The API stuff.
|
// The API stuff.
|
||||||
apiURL,
|
apiURL,
|
||||||
@ -570,6 +929,8 @@ func withBridge(
|
|||||||
mocks.ProxyCtl,
|
mocks.ProxyCtl,
|
||||||
mocks.CrashHandler,
|
mocks.CrashHandler,
|
||||||
mocks.Reporter,
|
mocks.Reporter,
|
||||||
|
testUIDValidityGenerator,
|
||||||
|
mocks.Heartbeat,
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||||
@ -583,17 +944,58 @@ func withBridge(
|
|||||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||||
|
|
||||||
// Set random IMAP and SMTP ports for the tests.
|
// Set random IMAP and SMTP ports for the tests.
|
||||||
require.NoError(t, bridge.SetIMAPPort(0))
|
require.NoError(t, bridge.SetIMAPPort(ctx, 0))
|
||||||
require.NoError(t, bridge.SetSMTPPort(0))
|
require.NoError(t, bridge.SetSMTPPort(ctx, 0))
|
||||||
|
|
||||||
|
if waitOnServers {
|
||||||
|
// Wait for bridge to start the IMAP server.
|
||||||
|
waitForEvent(t, eventCh, events.IMAPServerReady{})
|
||||||
|
// Wait for bridge to start the SMTP server.
|
||||||
|
waitForEvent(t, eventCh, events.SMTPServerReady{})
|
||||||
|
}
|
||||||
|
|
||||||
// Close the bridge when done.
|
// Close the bridge when done.
|
||||||
defer bridge.Close(ctx)
|
defer bridge.Close(ctx)
|
||||||
|
|
||||||
// Use the bridge.
|
// Use the bridge.
|
||||||
tests(bridge, mocks)
|
tests(bridge)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||||
|
func withBridge(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
apiURL string,
|
||||||
|
netCtl *proton.NetCtl,
|
||||||
|
locator bridge.Locator,
|
||||||
|
vaultKey []byte,
|
||||||
|
tests func(*bridge.Bridge, *bridge.Mocks),
|
||||||
|
) {
|
||||||
|
withMocks(t, func(mocks *bridge.Mocks) {
|
||||||
|
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
|
||||||
|
tests(bridge, mocks)
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// withBridgeWaitForServers is the same as withBridge, but it will wait until IMAP & SMTP servers are ready.
|
||||||
|
func withBridgeWaitForServers(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
apiURL string,
|
||||||
|
netCtl *proton.NetCtl,
|
||||||
|
locator bridge.Locator,
|
||||||
|
vaultKey []byte,
|
||||||
|
tests func(*bridge.Bridge, *bridge.Mocks),
|
||||||
|
) {
|
||||||
|
withMocks(t, func(mocks *bridge.Mocks) {
|
||||||
|
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
|
||||||
|
tests(bridge, mocks)
|
||||||
|
}, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, _ T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
for event := range eventCh {
|
for event := range eventCh {
|
||||||
@ -627,6 +1029,7 @@ func getConnectedUserIDs(t *testing.T, b *bridge.Bridge) []string {
|
|||||||
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
||||||
outCh := make(chan Out)
|
outCh := make(chan Out)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go func() {
|
go func() {
|
||||||
defer close(outCh)
|
defer close(outCh)
|
||||||
|
|
||||||
@ -636,9 +1039,116 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
|||||||
panic(fmt.Sprintf("unexpected type %T", in))
|
panic(fmt.Sprintf("unexpected type %T", in))
|
||||||
}
|
}
|
||||||
|
|
||||||
outCh <- out
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
case outCh <- out:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return outCh, done
|
return outCh, func() {
|
||||||
|
cancel()
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventWaiter struct {
|
||||||
|
evtCh <-chan events.Event
|
||||||
|
cancel func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventWaiter) Done() {
|
||||||
|
e.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventWaiter) Wait() {
|
||||||
|
<-e.evtCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForSMTPServerReady(b *bridge.Bridge) *eventWaiter {
|
||||||
|
evtCh, cancel := b.GetEvents(events.SMTPServerReady{})
|
||||||
|
return &eventWaiter{
|
||||||
|
evtCh: evtCh,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForSMTPServerStopped(b *bridge.Bridge) *eventWaiter {
|
||||||
|
evtCh, cancel := b.GetEvents(events.SMTPServerStopped{})
|
||||||
|
return &eventWaiter{
|
||||||
|
evtCh: evtCh,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForIMAPServerReady(b *bridge.Bridge) *eventWaiter {
|
||||||
|
evtCh, cancel := b.GetEvents(events.IMAPServerReady{})
|
||||||
|
return &eventWaiter{
|
||||||
|
evtCh: evtCh,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForIMAPServerStopped(b *bridge.Bridge) *eventWaiter {
|
||||||
|
evtCh, cancel := b.GetEvents(events.IMAPServerStopped{})
|
||||||
|
return &eventWaiter{
|
||||||
|
evtCh: evtCh,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_GetUpdatedCachePath(t *testing.T) {
|
||||||
|
type TestData struct {
|
||||||
|
gluonDBPath string
|
||||||
|
gluonCachePath string
|
||||||
|
shouldChange bool
|
||||||
|
}
|
||||||
|
|
||||||
|
dataArr := []TestData{
|
||||||
|
{
|
||||||
|
gluonDBPath: "/Users/test/",
|
||||||
|
gluonCachePath: "/Users/test/gluon",
|
||||||
|
shouldChange: false,
|
||||||
|
}, {
|
||||||
|
gluonDBPath: "/Users/test/",
|
||||||
|
gluonCachePath: "/Users/tester/gluon",
|
||||||
|
shouldChange: true,
|
||||||
|
}, {
|
||||||
|
gluonDBPath: "/Users/testing/",
|
||||||
|
gluonCachePath: "/Users/test/gluon",
|
||||||
|
shouldChange: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gluonDBPath: "/Users/testing/",
|
||||||
|
gluonCachePath: "/Users/test/gluon",
|
||||||
|
shouldChange: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gluonDBPath: "/Users/testing/",
|
||||||
|
gluonCachePath: "/Volumes/test/gluon",
|
||||||
|
shouldChange: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gluonDBPath: "/Volumes/test/",
|
||||||
|
gluonCachePath: "/Users/test/gluon",
|
||||||
|
shouldChange: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gluonDBPath: "/XXX/test/",
|
||||||
|
gluonCachePath: "/Users/test/gluon",
|
||||||
|
shouldChange: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gluonDBPath: "/XXX/test/",
|
||||||
|
gluonCachePath: "/YYY/test/gluon",
|
||||||
|
shouldChange: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, el := range dataArr {
|
||||||
|
newCachePath := bridge.GetUpdatedCachePath(el.gluonDBPath, el.gluonCachePath)
|
||||||
|
require.Equal(t, el.shouldChange, newCachePath != "" && newCachePath != el.gluonCachePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,13 +18,9 @@
|
|||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
@ -33,201 +29,131 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MaxTotalAttachmentSize = 7 * (1 << 20)
|
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
|
||||||
MaxCompressedFilesCount = 6
|
DefaultMaxSessionCountForBugReport = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { //nolint:funlen
|
type ReportBugReq struct {
|
||||||
var account string
|
OSType string
|
||||||
|
OSVersion string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
EmailClient string
|
||||||
|
IncludeLogs bool
|
||||||
|
}
|
||||||
|
|
||||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
|
||||||
account = info.Username
|
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
|
||||||
|
report.Username = info.Username
|
||||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||||
account = user.Username()
|
report.Username = user.Username()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var atts []proton.ReportBugAttachment
|
var attachments []proton.ReportBugAttachment
|
||||||
|
if report.IncludeLogs {
|
||||||
if attachLogs {
|
logs, err := bridge.CollectLogs()
|
||||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
|
||||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
attachments = append(attachments, logs)
|
||||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
|
||||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
|
||||||
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var matchFiles []string
|
|
||||||
|
|
||||||
// Include bridge logs, up to a maximum amount.
|
|
||||||
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
|
|
||||||
|
|
||||||
// Include crash logs, up to a maximum amount.
|
|
||||||
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
|
|
||||||
|
|
||||||
// bridge-gui keeps just one small (~ 1kb) log file; we always include it.
|
|
||||||
if len(guiLogs) > 0 {
|
|
||||||
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
archive, err := zipFiles(matchFiles)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(archive)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
atts = append(atts, proton.ReportBugAttachment{
|
|
||||||
Name: "logs.zip",
|
|
||||||
Filename: "logs.zip",
|
|
||||||
MIMEType: "application/zip",
|
|
||||||
Body: body,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
var firstAtt proton.ReportBugAttachment
|
||||||
OS: osType,
|
if len(attachments) > 0 && report.IncludeLogs {
|
||||||
OSVersion: osVersion,
|
firstAtt = attachments[0]
|
||||||
|
}
|
||||||
|
|
||||||
Title: "[Bridge] Bug",
|
attachmentType := proton.AttachmentTypeSync
|
||||||
Description: description,
|
if len(attachments) > 1 {
|
||||||
|
attachmentType = proton.AttachmentTypeAsync
|
||||||
|
}
|
||||||
|
|
||||||
Client: client,
|
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
|
||||||
|
if err != nil || token == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a token we can append more attachment to the bugReport
|
||||||
|
for i, att := range attachments {
|
||||||
|
if i == 0 && report.IncludeLogs {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := bridge.appendComment(ctx, token, att)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
|
||||||
|
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return proton.ReportBugAttachment{
|
||||||
|
Name: "logs.zip",
|
||||||
|
Filename: "logs.zip",
|
||||||
|
MIMEType: "application/zip",
|
||||||
|
Body: body,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
|
||||||
|
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
|
||||||
|
var attachments []proton.ReportBugAttachment
|
||||||
|
attachments = append(attachments, att)
|
||||||
|
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||||
|
OS: report.OSType,
|
||||||
|
OSVersion: report.OSVersion,
|
||||||
|
|
||||||
|
Title: "[Bridge] Bug - " + report.Title,
|
||||||
|
Description: report.Description,
|
||||||
|
|
||||||
|
Client: report.EmailClient,
|
||||||
ClientType: proton.ClientTypeEmail,
|
ClientType: proton.ClientTypeEmail,
|
||||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||||
|
|
||||||
Username: account,
|
Username: report.Username,
|
||||||
Email: email,
|
Email: report.Email,
|
||||||
}, atts...)
|
|
||||||
|
AsyncAttachments: asyncAttach,
|
||||||
|
}, attachments...)
|
||||||
|
|
||||||
|
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
|
||||||
|
return "", errors.New("no token returns for AsyncAttachments")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *res.Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func max(a, b int) int {
|
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
|
||||||
if a > b {
|
var attachments []proton.ReportBugAttachment
|
||||||
return a
|
attachments = append(attachments, att)
|
||||||
}
|
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
|
||||||
|
Product: proton.ClientTypeEmail,
|
||||||
return b
|
Body: "Comment adding attachment: " + att.Filename,
|
||||||
}
|
Token: token,
|
||||||
|
}, attachments...)
|
||||||
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
|
||||||
logsPath, err := locator.ProvideLogsPath()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := os.ReadDir(logsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var matchFiles []string
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if filenameMatchFunc(file.Name()) {
|
|
||||||
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
|
|
||||||
|
|
||||||
return matchFiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type limitedBuffer struct {
|
|
||||||
capacity int
|
|
||||||
buf *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLimitedBuffer(capacity int) *limitedBuffer {
|
|
||||||
return &limitedBuffer{
|
|
||||||
capacity: capacity,
|
|
||||||
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
|
||||||
if len(p)+b.buf.Len() > b.capacity {
|
|
||||||
return 0, ErrSizeTooLarge
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.buf.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
|
|
||||||
return b.buf.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func zipFiles(filenames []string) (io.Reader, error) {
|
|
||||||
if len(filenames) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := newLimitedBuffer(MaxTotalAttachmentSize)
|
|
||||||
|
|
||||||
w := zip.NewWriter(buf)
|
|
||||||
defer w.Close() //nolint:errcheck
|
|
||||||
|
|
||||||
for _, file := range filenames {
|
|
||||||
if err := addFileToZip(file, w); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addFileToZip(filename string, writer *zip.Writer) error {
|
|
||||||
fileReader, err := os.Open(filepath.Clean(filename))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fileReader.Close() //nolint:errcheck,gosec
|
|
||||||
|
|
||||||
fileInfo, err := fileReader.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
header, err := zip.FileInfoHeader(fileInfo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
header.Method = zip.Deflate
|
|
||||||
header.Name = filepath.Base(filename)
|
|
||||||
|
|
||||||
fileWriter, err := writer.CreateHeader(header)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.Copy(fileWriter, fileReader); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileReader.Close()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,6 +18,8 @@
|
|||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
|
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
|
||||||
@ -29,10 +31,10 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigureAppleMail configures apple mail for the given userID and address.
|
// ConfigureAppleMail configures Apple Mail for the given userID and address.
|
||||||
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
|
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL.
|
||||||
func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
|
||||||
logrus.WithFields(logrus.Fields{
|
logPkg.WithFields(logrus.Fields{
|
||||||
"userID": userID,
|
"userID": userID,
|
||||||
"address": logging.Sensitive(address),
|
"address": logging.Sensitive(address),
|
||||||
}).Info("Configuring Apple Mail")
|
}).Info("Configuring Apple Mail")
|
||||||
@ -43,20 +45,32 @@ func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
|||||||
return ErrNoSuchUser
|
return ErrNoSuchUser
|
||||||
}
|
}
|
||||||
|
|
||||||
if address == "" {
|
emails := user.Emails()
|
||||||
address = user.Emails()[0]
|
displayNames := user.DisplayNames()
|
||||||
|
if (len(emails) == 0) || (len(displayNames) == 0) {
|
||||||
|
return errors.New("could not retrieve user address info")
|
||||||
}
|
}
|
||||||
|
|
||||||
username := address
|
if address == "" {
|
||||||
addresses := address
|
address = emails[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var username, displayName, addresses string
|
||||||
if user.GetAddressMode() == vault.CombinedMode {
|
if user.GetAddressMode() == vault.CombinedMode {
|
||||||
username = user.Emails()[0]
|
username = address
|
||||||
addresses = strings.Join(user.Emails(), ",")
|
displayName = displayNames[username]
|
||||||
|
addresses = strings.Join(emails, ",")
|
||||||
|
} else {
|
||||||
|
username = address
|
||||||
|
addresses = address
|
||||||
|
displayName = displayNames[address]
|
||||||
|
if len(displayName) == 0 {
|
||||||
|
displayName = address
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||||
if err := bridge.SetSMTPSSL(true); err != nil {
|
if err := bridge.SetSMTPSSL(ctx, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,6 +82,7 @@ func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
|||||||
bridge.vault.GetIMAPSSL(),
|
bridge.vault.GetIMAPSSL(),
|
||||||
bridge.vault.GetSMTPSSL(),
|
bridge.vault.GetSMTPSSL(),
|
||||||
username,
|
username,
|
||||||
|
displayName,
|
||||||
addresses,
|
addresses,
|
||||||
user.BridgePass(),
|
user.BridgePass(),
|
||||||
)
|
)
|
||||||
|
|||||||
301
internal/bridge/debug.go
Normal file
301
internal/bridge/debug.go
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
|
"github.com/bradenaw/juniper/iterator"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
goimap "github.com/emersion/go-imap"
|
||||||
|
goimapclient "github.com/emersion/go-imap/client"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckClientStateResult struct {
|
||||||
|
MissingMessages map[string]map[string]user.DiagMailboxMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
|
||||||
|
v, ok := c.MissingMessages[userID]
|
||||||
|
if !ok {
|
||||||
|
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
|
||||||
|
} else {
|
||||||
|
v[message.ID] = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
|
||||||
|
// anything that is out of place.
|
||||||
|
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) (CheckClientStateResult, error) {
|
||||||
|
bridge.usersLock.RLock()
|
||||||
|
defer bridge.usersLock.RUnlock()
|
||||||
|
|
||||||
|
users := maps.Values(bridge.users)
|
||||||
|
|
||||||
|
result := CheckClientStateResult{
|
||||||
|
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, usr := range users {
|
||||||
|
if progressCB != nil {
|
||||||
|
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
|
||||||
|
}
|
||||||
|
log := logrus.WithFields(logrus.Fields{
|
||||||
|
"pkg": "bridge/debug",
|
||||||
|
"user": usr.Name(),
|
||||||
|
"diag": "state-check",
|
||||||
|
})
|
||||||
|
log.Debug("Retrieving all server metadata")
|
||||||
|
meta, err := usr.GetDiagnosticMetadata(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
success := true
|
||||||
|
|
||||||
|
if len(meta.Metadata) != len(meta.MessageIDs) {
|
||||||
|
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Building state")
|
||||||
|
state, err := meta.BuildMailboxToMessageMap(ctx, usr)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Failed to build state")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := bridge.GetUserInfo(usr.ID())
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Failed to get user info")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
|
||||||
|
|
||||||
|
for account, mboxMap := range state {
|
||||||
|
if progressCB != nil {
|
||||||
|
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
|
||||||
|
}
|
||||||
|
if err := func(account string, mboxMap user.AccountMailboxMap) error {
|
||||||
|
client, err := goimapclient.Dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Failed to connect to imap client")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = client.Logout()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := client.Login(account, string(info.BridgePass)); err != nil {
|
||||||
|
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log := log.WithField("account", account)
|
||||||
|
for mboxName, messageList := range mboxMap {
|
||||||
|
log := log.WithField("mbox", mboxName)
|
||||||
|
status, err := client.Select(mboxName, true)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
|
||||||
|
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Checking message count")
|
||||||
|
|
||||||
|
if int(status.Messages) != len(messageList) {
|
||||||
|
success = false
|
||||||
|
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := clientGetMessageIDs(client, mboxName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msg := range messageList {
|
||||||
|
imapFlags, ok := ids[msg.ID]
|
||||||
|
if !ok {
|
||||||
|
if meta.FailedMessageIDs.Contains(msg.ID) {
|
||||||
|
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
|
||||||
|
} else {
|
||||||
|
log.Errorf("Missing message '%v'", msg.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.AddMissingMessage(msg.UserID, msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkFlags {
|
||||||
|
if !imapFlags.Equals(msg.Flags) {
|
||||||
|
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
|
||||||
|
msg.ID,
|
||||||
|
imapFlags.ToSlice(),
|
||||||
|
msg.Flags.ToSlice(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
log.Errorf("State does not match")
|
||||||
|
} else {
|
||||||
|
log.Info("State matches")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}(account, mboxMap); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for orphaned messages (only present in All Mail)
|
||||||
|
if progressCB != nil {
|
||||||
|
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
|
||||||
|
}
|
||||||
|
log.Debugf("Checking for orphans")
|
||||||
|
|
||||||
|
for _, m := range meta.Metadata {
|
||||||
|
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
|
||||||
|
switch t {
|
||||||
|
case proton.AllMailLabel:
|
||||||
|
return false
|
||||||
|
case proton.AllSentLabel:
|
||||||
|
return false
|
||||||
|
case proton.AllDraftsLabel:
|
||||||
|
return false
|
||||||
|
case proton.OutboxLabel:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(filteredLabels) == 0 {
|
||||||
|
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) DebugDownloadFailedMessages(
|
||||||
|
ctx context.Context,
|
||||||
|
result CheckClientStateResult,
|
||||||
|
exportPath string,
|
||||||
|
progressCB func(string, int, int),
|
||||||
|
) error {
|
||||||
|
bridge.usersLock.RLock()
|
||||||
|
defer bridge.usersLock.RUnlock()
|
||||||
|
|
||||||
|
for userID, messages := range result.MissingMessages {
|
||||||
|
usr, ok := bridge.users[userID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("failed to find user with id %v", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
userDir := filepath.Join(exportPath, userID)
|
||||||
|
if err := os.MkdirAll(userDir, 0o700); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
||||||
|
status, err := client.Select(mailbox, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Messages == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resCh := make(chan *goimap.Message)
|
||||||
|
|
||||||
|
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
|
||||||
|
|
||||||
|
seq, err := goimap.ParseSeqSet("1:*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := client.Fetch(
|
||||||
|
seq,
|
||||||
|
fetchItems,
|
||||||
|
resCh,
|
||||||
|
); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
messages := iterator.Collect(iterator.Chan(resCh))
|
||||||
|
|
||||||
|
ids := make(map[string]imap.FlagSet, len(messages))
|
||||||
|
|
||||||
|
for i, m := range messages {
|
||||||
|
literal, err := io.ReadAll(m.GetBody(section))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := rfc822.NewHeader(literal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
|
||||||
|
if !ok {
|
||||||
|
logrus.WithField("pkg", "bridge/debug").Errorf("Message %v does not have internal id", internalID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
messageFlags := imap.NewFlagSet(m.Flags...)
|
||||||
|
|
||||||
|
// Recent and Deleted are not part of the proton flag set.
|
||||||
|
messageFlags.RemoveFromSelf("\\Recent")
|
||||||
|
messageFlags.RemoveFromSelf("\\Deleted")
|
||||||
|
|
||||||
|
ids[internalID] = messageFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
171
internal/bridge/draft_test.go
Normal file
171
internal/bridge/draft_test.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
go_imap "github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
|
||||||
|
getGluonHeaderID := func(literal []byte) (string, string) {
|
||||||
|
h, err := rfc822.NewHeader(literal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
externalID, ok := h.GetChecked("Message-Id")
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
return gluonID, externalID
|
||||||
|
}
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
_, _, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, err = s.CreateUser("bar", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
// Create first draft in client.
|
||||||
|
literal := fmt.Sprintf(`From: %v
|
||||||
|
To: %v
|
||||||
|
Date: Fri, 3 Feb 2023 01:04:32 +0100
|
||||||
|
Subject: Foo
|
||||||
|
|
||||||
|
Hello
|
||||||
|
`, info.Addresses[0], "bar@proton.local")
|
||||||
|
|
||||||
|
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
|
||||||
|
// Verify the draft is available in client.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 1
|
||||||
|
}, 2*time.Second, time.Second)
|
||||||
|
|
||||||
|
// Retrieve the new literal so we can have the Proton Message ID.
|
||||||
|
messages, err := clientFetch(client, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(messages))
|
||||||
|
|
||||||
|
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||||
|
require.NoError(t, err)
|
||||||
|
logrus.Info(string(newLiteral))
|
||||||
|
|
||||||
|
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
|
||||||
|
|
||||||
|
// Modify new literal.
|
||||||
|
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
|
||||||
|
|
||||||
|
func() {
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = smtpClient.Close() }()
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL PLAIN.
|
||||||
|
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||||
|
info.Addresses[0],
|
||||||
|
info.Addresses[0],
|
||||||
|
string(info.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, smtpClient.SendMail(
|
||||||
|
info.Addresses[0],
|
||||||
|
[]string{"bar@proton.local"},
|
||||||
|
bytes.NewReader(newLiteralModified),
|
||||||
|
))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Append message to Sent as the imap client would.
|
||||||
|
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
|
||||||
|
|
||||||
|
// Verify the sent message gets updated with the new literal.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
// Check if sent message matches the latest draft.
|
||||||
|
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if len(messagesClient1) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
|
||||||
|
|
||||||
|
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sentLiteralStr := string(sentLiteralText)
|
||||||
|
|
||||||
|
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
|
||||||
|
|
||||||
|
idIsDifferent := sentLiteralID != newLiteralID
|
||||||
|
|
||||||
|
externIDMatches := sentLiteralExternID == newLiteralExternID
|
||||||
|
|
||||||
|
return literalMatches && idIsDifferent && externIDMatches
|
||||||
|
}, 2*time.Second, time.Second)
|
||||||
|
})
|
||||||
|
}, server.WithMessageDedup())
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -22,10 +22,7 @@ import "errors"
|
|||||||
var (
|
var (
|
||||||
ErrVaultInsecure = errors.New("the vault is insecure")
|
ErrVaultInsecure = errors.New("the vault is insecure")
|
||||||
ErrVaultCorrupt = errors.New("the vault is corrupt")
|
ErrVaultCorrupt = errors.New("the vault is corrupt")
|
||||||
|
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||||
ErrServeIMAP = errors.New("failed to serve IMAP")
|
|
||||||
ErrServeSMTP = errors.New("failed to serve SMTP")
|
|
||||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
|
||||||
|
|
||||||
ErrNoSuchUser = errors.New("no such user")
|
ErrNoSuchUser = errors.New("no such user")
|
||||||
ErrUserAlreadyExists = errors.New("user already exists")
|
ErrUserAlreadyExists = errors.New("user already exists")
|
||||||
|
|||||||
45
internal/bridge/events.go
Normal file
45
internal/bridge/events.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bridgeEventSubscription struct {
|
||||||
|
b *Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeEventSubscription) Add(ofType ...events.Event) *watcher.Watcher[events.Event] {
|
||||||
|
return b.b.addWatcher(ofType...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeEventSubscription) Remove(watcher *watcher.Watcher[events.Event]) {
|
||||||
|
b.b.remWatcher(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
type bridgeEventPublisher struct {
|
||||||
|
b *Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeEventPublisher) PublishEvent(_ context.Context, event events.Event) {
|
||||||
|
b.b.publish(event)
|
||||||
|
}
|
||||||
167
internal/bridge/heartbeat.go
Normal file
167
internal/bridge/heartbeat.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const HeartbeatCheckInterval = time.Hour
|
||||||
|
|
||||||
|
type heartBeatState struct {
|
||||||
|
task *async.Group
|
||||||
|
telemetry.Heartbeat
|
||||||
|
taskLock sync.Mutex
|
||||||
|
taskStarted bool
|
||||||
|
taskInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHeartBeatState(ctx context.Context, panicHandler async.PanicHandler) *heartBeatState {
|
||||||
|
return &heartBeatState{
|
||||||
|
task: async.NewGroup(ctx, panicHandler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager) {
|
||||||
|
h.Heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), bridge.keychains.GetDefaultHelper())
|
||||||
|
h.taskInterval = manager.GetHeartbeatPeriodicInterval()
|
||||||
|
h.SetRollout(bridge.GetUpdateRollout())
|
||||||
|
h.SetAutoStart(bridge.GetAutostart())
|
||||||
|
h.SetAutoUpdate(bridge.GetAutoUpdate())
|
||||||
|
h.SetBeta(bridge.GetUpdateChannel())
|
||||||
|
h.SetDoh(bridge.GetProxyAllowed())
|
||||||
|
h.SetShowAllMail(bridge.GetShowAllMail())
|
||||||
|
h.SetIMAPConnectionMode(bridge.GetIMAPSSL())
|
||||||
|
h.SetSMTPConnectionMode(bridge.GetSMTPSSL())
|
||||||
|
h.SetIMAPPort(bridge.GetIMAPPort())
|
||||||
|
h.SetSMTPPort(bridge.GetSMTPPort())
|
||||||
|
h.SetCacheLocation(bridge.GetGluonCacheDir())
|
||||||
|
if val, err := bridge.GetKeychainApp(); err != nil {
|
||||||
|
h.SetKeyChainPref(val)
|
||||||
|
} else {
|
||||||
|
h.SetKeyChainPref(bridge.keychains.GetDefaultHelper())
|
||||||
|
}
|
||||||
|
h.SetPrevVersion(bridge.GetLastVersion().String())
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
|
var splitMode = false
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
if user.GetAddressMode() == vault.SplitMode {
|
||||||
|
splitMode = true
|
||||||
|
}
|
||||||
|
h.SetUserPlan(user.GetUserPlanName())
|
||||||
|
}
|
||||||
|
var numberConnectedAccounts = len(bridge.users)
|
||||||
|
h.SetNumberConnectedAccounts(numberConnectedAccounts)
|
||||||
|
h.SetSplitMode(splitMode)
|
||||||
|
|
||||||
|
// Do not try to send if there is no user yet.
|
||||||
|
if numberConnectedAccounts > 0 {
|
||||||
|
defer h.start()
|
||||||
|
}
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *heartBeatState) start() {
|
||||||
|
h.taskLock.Lock()
|
||||||
|
defer h.taskLock.Unlock()
|
||||||
|
if h.taskStarted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.taskStarted = true
|
||||||
|
|
||||||
|
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
|
||||||
|
logrus.WithField("pkg", "bridge/heartbeat").Debug("Checking for heartbeat")
|
||||||
|
|
||||||
|
h.TrySending(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *heartBeatState) stop() {
|
||||||
|
h.taskLock.Lock()
|
||||||
|
defer h.taskLock.Unlock()
|
||||||
|
if !h.taskStarted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.task.CancelAndWait()
|
||||||
|
h.taskStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
|
||||||
|
var flag = true
|
||||||
|
if bridge.GetTelemetryDisabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
flag = flag && user.IsTelemetryEnabled(ctx)
|
||||||
|
}
|
||||||
|
}, bridge.usersLock)
|
||||||
|
|
||||||
|
return flag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
|
||||||
|
data, err := json.Marshal(heartbeat)
|
||||||
|
if err != nil {
|
||||||
|
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
|
||||||
|
"error": err,
|
||||||
|
}); err != nil {
|
||||||
|
logrus.WithField("pkg", "bridge/heartbeat").WithError(err).Error("Failed to parse heartbeat data.")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var sent = false
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
if err := user.SendTelemetry(ctx, data); err == nil {
|
||||||
|
sent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, bridge.usersLock)
|
||||||
|
|
||||||
|
return sent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetLastHeartbeatSent() time.Time {
|
||||||
|
return bridge.vault.GetLastHeartbeatSent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
|
||||||
|
return bridge.vault.SetLastHeartbeatSent(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
|
||||||
|
return HeartbeatCheckInterval
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -17,6 +17,10 @@
|
|||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) GetCurrentUserAgent() string {
|
func (bridge *Bridge) GetCurrentUserAgent() string {
|
||||||
return bridge.identifier.GetUserAgent()
|
return bridge.identifier.GetUserAgent()
|
||||||
}
|
}
|
||||||
@ -24,3 +28,52 @@ func (bridge *Bridge) GetCurrentUserAgent() string {
|
|||||||
func (bridge *Bridge) SetCurrentPlatform(platform string) {
|
func (bridge *Bridge) SetCurrentPlatform(platform string) {
|
||||||
bridge.identifier.SetPlatform(platform)
|
bridge.identifier.SetPlatform(platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) setUserAgent(name, version string) {
|
||||||
|
currentUserAgent := bridge.identifier.GetClientString()
|
||||||
|
|
||||||
|
bridge.heartbeat.SetContactedByAppleNotes(name)
|
||||||
|
|
||||||
|
bridge.identifier.SetClient(name, version)
|
||||||
|
|
||||||
|
newUserAgent := bridge.identifier.GetClientString()
|
||||||
|
|
||||||
|
if currentUserAgent != newUserAgent {
|
||||||
|
if err := bridge.vault.SetLastUserAgent(newUserAgent); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to write new user agent to vault")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type bridgeUserAgentUpdater struct {
|
||||||
|
*Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) GetUserAgent() string {
|
||||||
|
return b.identifier.GetUserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) HasClient() bool {
|
||||||
|
return b.identifier.HasClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
|
||||||
|
b.heartbeat.SetContactedByAppleNotes(name)
|
||||||
|
b.identifier.SetClient(name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetPlatform(platform string) {
|
||||||
|
b.identifier.SetPlatform(platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetClientString(client string) {
|
||||||
|
b.identifier.SetClientString(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) GetClientString() string {
|
||||||
|
return b.identifier.GetClientString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetUserAgent(name, version string) {
|
||||||
|
b.setUserAgent(name, version)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -20,312 +20,117 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"strings"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/gluon/store"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func (bridge *Bridge) restartIMAP(ctx context.Context) error {
|
||||||
defaultClientName = "UnknownClient"
|
return bridge.serverManager.RestartIMAP(ctx)
|
||||||
defaultClientVersion = "0.0.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (bridge *Bridge) serveIMAP() error {
|
|
||||||
if bridge.imapServer == nil {
|
|
||||||
return fmt.Errorf("no imap server instance running")
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Info("Starting IMAP server")
|
|
||||||
|
|
||||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create IMAP listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.imapListener = imapListener
|
|
||||||
|
|
||||||
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
|
||||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
|
||||||
return fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) restartIMAP() error {
|
|
||||||
logrus.Info("Restarting IMAP server")
|
|
||||||
|
|
||||||
if bridge.imapListener != nil {
|
|
||||||
if err := bridge.imapListener.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bridge.serveIMAP()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
|
||||||
logrus.Info("Closing IMAP server")
|
|
||||||
|
|
||||||
if bridge.imapServer != nil {
|
|
||||||
if err := bridge.imapServer.Close(ctx); err != nil {
|
|
||||||
return fmt.Errorf("failed to close IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
bridge.imapServer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if bridge.imapListener != nil {
|
|
||||||
if err := bridge.imapListener.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addIMAPUser connects the given user to gluon.
|
|
||||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
|
||||||
if bridge.imapServer == nil {
|
|
||||||
return fmt.Errorf("no imap server instance running")
|
|
||||||
}
|
|
||||||
|
|
||||||
imapConn, err := user.NewIMAPConnectors()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create IMAP connectors: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for addrID, imapConn := range imapConn {
|
|
||||||
log := logrus.WithFields(logrus.Fields{
|
|
||||||
"userID": user.ID(),
|
|
||||||
"addrID": addrID,
|
|
||||||
})
|
|
||||||
|
|
||||||
if gluonID, ok := user.GetGluonID(addrID); ok {
|
|
||||||
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
|
||||||
|
|
||||||
if err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
|
||||||
return fmt.Errorf("failed to load IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Info("Creating new IMAP user")
|
|
||||||
|
|
||||||
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
|
|
||||||
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
|
||||||
if bridge.imapServer == nil {
|
|
||||||
return fmt.Errorf("no imap server instance running")
|
|
||||||
}
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"userID": user.ID(),
|
|
||||||
"withData": withData,
|
|
||||||
}).Debug("Removing IMAP user")
|
|
||||||
|
|
||||||
for addrID, gluonID := range user.GetGluonIDs() {
|
|
||||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if withData {
|
|
||||||
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||||
|
log := logrus.WithField("pkg", "bridge/event/imap")
|
||||||
|
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case imapEvents.UserAdded:
|
case imapEvents.UserAdded:
|
||||||
for labelID, count := range event.Counts {
|
for labelID, count := range event.Counts {
|
||||||
logrus.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
"gluonID": event.UserID,
|
"gluonID": event.UserID,
|
||||||
"labelID": labelID,
|
"labelID": labelID,
|
||||||
"count": count,
|
"count": count,
|
||||||
}).Info("Received mailbox message count")
|
}).Info("Received mailbox message count")
|
||||||
}
|
}
|
||||||
|
|
||||||
case imapEvents.SessionAdded:
|
|
||||||
if !bridge.identifier.HasClient() {
|
|
||||||
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
case imapEvents.IMAPID:
|
case imapEvents.IMAPID:
|
||||||
logrus.WithFields(logrus.Fields{
|
log.WithFields(logrus.Fields{
|
||||||
"sessionID": event.SessionID,
|
"sessionID": event.SessionID,
|
||||||
"name": event.IMAPID.Name,
|
"name": event.IMAPID.Name,
|
||||||
"version": event.IMAPID.Version,
|
"version": event.IMAPID.Version,
|
||||||
}).Info("Received IMAP ID")
|
}).Info("Received IMAP ID")
|
||||||
|
|
||||||
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
|
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
|
||||||
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
bridge.setUserAgent(event.IMAPID.Name, event.IMAPID.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
case imapEvents.LoginFailed:
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"sessionID": event.SessionID,
|
||||||
|
"username": event.Username,
|
||||||
|
"pkg": "imap",
|
||||||
|
}).Error("Incorrect login credentials.")
|
||||||
|
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
|
||||||
|
|
||||||
|
case imapEvents.Login:
|
||||||
|
if strings.Contains(bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
|
||||||
|
bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGluonDir(encVault *vault.Vault) (string, error) {
|
type bridgeIMAPSettings struct {
|
||||||
empty, exists, err := isEmpty(encVault.GetGluonDir())
|
b *Bridge
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to check if gluon dir is empty: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
if err := os.MkdirAll(encVault.GetGluonDir(), 0o700); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if empty {
|
|
||||||
if err := encVault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
|
||||||
return user.ClearSyncStatus()
|
|
||||||
}); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to reset user sync status: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return encVault.GetGluonDir(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
|
||||||
func newIMAPServer(
|
return b
|
||||||
gluonDir string,
|
|
||||||
version *semver.Version,
|
|
||||||
tlsConfig *tls.Config,
|
|
||||||
reporter reporter.Reporter,
|
|
||||||
logClient, logServer bool,
|
|
||||||
eventCh chan<- imapEvents.Event,
|
|
||||||
tasks *async.Group,
|
|
||||||
) (*gluon.Server, error) {
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"gluonDir": gluonDir,
|
|
||||||
"version": version,
|
|
||||||
"logClient": logClient,
|
|
||||||
"logServer": logServer,
|
|
||||||
}).Info("Creating IMAP server")
|
|
||||||
|
|
||||||
if logClient || logServer {
|
|
||||||
log := logrus.WithField("protocol", "IMAP")
|
|
||||||
log.Warning("================================================")
|
|
||||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
|
||||||
log.Warning("================================================")
|
|
||||||
}
|
|
||||||
|
|
||||||
var imapClientLog io.Writer
|
|
||||||
|
|
||||||
if logClient {
|
|
||||||
imapClientLog = logging.NewIMAPLogger()
|
|
||||||
} else {
|
|
||||||
imapClientLog = io.Discard
|
|
||||||
}
|
|
||||||
|
|
||||||
var imapServerLog io.Writer
|
|
||||||
|
|
||||||
if logServer {
|
|
||||||
imapServerLog = logging.NewIMAPLogger()
|
|
||||||
} else {
|
|
||||||
imapServerLog = io.Discard
|
|
||||||
}
|
|
||||||
|
|
||||||
imapServer, err := gluon.New(
|
|
||||||
gluon.WithTLS(tlsConfig),
|
|
||||||
gluon.WithDataDir(gluonDir),
|
|
||||||
gluon.WithStoreBuilder(new(storeBuilder)),
|
|
||||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
|
||||||
getGluonVersionInfo(version),
|
|
||||||
gluon.WithReporter(reporter),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.Once(func(ctx context.Context) {
|
|
||||||
async.ForwardContext(ctx, eventCh, imapServer.AddWatcher())
|
|
||||||
})
|
|
||||||
|
|
||||||
tasks.Once(func(ctx context.Context) {
|
|
||||||
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
|
|
||||||
logrus.WithError(err).Error("IMAP server error")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return imapServer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGluonVersionInfo(version *semver.Version) gluon.Option {
|
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
|
||||||
return gluon.WithVersionInfo(
|
return b.b.tlsConfig
|
||||||
int(version.Major()),
|
|
||||||
int(version.Minor()),
|
|
||||||
int(version.Patch()),
|
|
||||||
constants.FullAppName,
|
|
||||||
"TODO",
|
|
||||||
"TODO",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isEmpty returns whether the given directory is empty.
|
func (b *bridgeIMAPSettings) LogClient() bool {
|
||||||
// If the directory does not exist, the second return value is false.
|
return b.b.logIMAPClient
|
||||||
func isEmpty(dir string) (bool, bool, error) {
|
}
|
||||||
if _, err := os.Stat(dir); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return false, false, fmt.Errorf("failed to stat %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, false, nil
|
func (b *bridgeIMAPSettings) LogServer() bool {
|
||||||
|
return b.b.logIMAPServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) DisableIMAPAuthenticate() bool {
|
||||||
|
return b.b.unleashService.GetFlagValue(unleash.IMAPAuthenticateCommandDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) Port() int {
|
||||||
|
return b.b.vault.GetIMAPPort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) SetPort(i int) error {
|
||||||
|
return b.b.vault.SetIMAPPort(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) UseSSL() bool {
|
||||||
|
return b.b.vault.GetIMAPSSL()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) CacheDirectory() string {
|
||||||
|
return b.b.GetGluonCacheDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) DataDirectory() (string, error) {
|
||||||
|
return b.b.GetGluonDataDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) SetCacheDirectory(s string) error {
|
||||||
|
return b.b.vault.SetGluonDir(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) Version() *semver.Version {
|
||||||
|
return b.b.curVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) PublishIMAPEvent(ctx context.Context, event imapEvents.Event) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case b.b.imapEventCh <- event:
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, fmt.Errorf("failed to read dir %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(entries) == 0, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type storeBuilder struct{}
|
|
||||||
|
|
||||||
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
|
||||||
return store.NewOnDiskStore(
|
|
||||||
filepath.Join(path, userID),
|
|
||||||
passphrase,
|
|
||||||
store.WithCompressor(new(store.GZipCompressor)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*storeBuilder) Delete(path, userID string) error {
|
|
||||||
return os.RemoveAll(filepath.Join(path, userID))
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
internal/bridge/imapsmtp_telemetry.go
Normal file
26
internal/bridge/imapsmtp_telemetry.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
type bridgeIMAPSMTPTelemetry struct {
|
||||||
|
b *Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeIMAPSMTPTelemetry) SetCacheLocation(s string) {
|
||||||
|
b.b.heartbeat.SetCacheLocation(s)
|
||||||
|
}
|
||||||
24
internal/bridge/keychain.go
Normal file
24
internal/bridge/keychain.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import "golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetHelpersNames() []string {
|
||||||
|
return maps.Keys(bridge.keychains.GetHelpers())
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||||
@ -24,6 +25,7 @@ type Mocks struct {
|
|||||||
|
|
||||||
CrashHandler *mocks.MockPanicHandler
|
CrashHandler *mocks.MockPanicHandler
|
||||||
Reporter *mocks.MockReporter
|
Reporter *mocks.MockReporter
|
||||||
|
Heartbeat *mocks.MockHeartbeatManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||||
@ -39,13 +41,18 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
|||||||
|
|
||||||
CrashHandler: mocks.NewMockPanicHandler(ctl),
|
CrashHandler: mocks.NewMockPanicHandler(ctl),
|
||||||
Reporter: mocks.NewMockReporter(ctl),
|
Reporter: mocks.NewMockReporter(ctl),
|
||||||
|
Heartbeat: mocks.NewMockHeartbeatManager(ctl),
|
||||||
}
|
}
|
||||||
|
|
||||||
// When getting the TLS issue channel, we want to return the test channel.
|
// When getting the TLS issue channel, we want to return the test channel.
|
||||||
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
||||||
|
|
||||||
// This is called at he end of any go-routine:
|
// This is called at the end of any go-routine:
|
||||||
mocks.CrashHandler.EXPECT().HandlePanic().AnyTimes()
|
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
|
// this is called at start of heartbeat process.
|
||||||
|
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||||
|
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
|
||||||
|
|
||||||
return mocks
|
return mocks
|
||||||
}
|
}
|
||||||
@ -112,13 +119,14 @@ func (provider *TestLocationsProvider) UserCache() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TestUpdater struct {
|
type TestUpdater struct {
|
||||||
latest updater.VersionInfo
|
latest updater.VersionInfoLegacy
|
||||||
lock sync.RWMutex
|
releases updater.VersionInfo
|
||||||
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
||||||
return &TestUpdater{
|
return &TestUpdater{
|
||||||
latest: updater.VersionInfo{
|
latest: updater.VersionInfoLegacy{
|
||||||
Version: version,
|
Version: version,
|
||||||
MinAuto: minAuto,
|
MinAuto: minAuto,
|
||||||
|
|
||||||
@ -127,11 +135,11 @@ func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) {
|
func (testUpdater *TestUpdater) SetLatestVersionLegacy(version, minAuto *semver.Version) {
|
||||||
testUpdater.lock.Lock()
|
testUpdater.lock.Lock()
|
||||||
defer testUpdater.lock.Unlock()
|
defer testUpdater.lock.Unlock()
|
||||||
|
|
||||||
testUpdater.latest = updater.VersionInfo{
|
testUpdater.latest = updater.VersionInfoLegacy{
|
||||||
Version: version,
|
Version: version,
|
||||||
MinAuto: minAuto,
|
MinAuto: minAuto,
|
||||||
|
|
||||||
@ -139,13 +147,35 @@ func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Versio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) GetVersionInfo(ctx context.Context, downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) {
|
func (testUpdater *TestUpdater) GetVersionInfoLegacy(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfoLegacy, error) {
|
||||||
testUpdater.lock.RLock()
|
testUpdater.lock.RLock()
|
||||||
defer testUpdater.lock.RUnlock()
|
defer testUpdater.lock.RUnlock()
|
||||||
|
|
||||||
return testUpdater.latest, nil
|
return testUpdater.latest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) InstallUpdate(ctx context.Context, downloader updater.Downloader, update updater.VersionInfo) error {
|
func (testUpdater *TestUpdater) InstallUpdateLegacy(_ context.Context, _ updater.Downloader, _ updater.VersionInfoLegacy) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) SetLatestVersion(releases updater.VersionInfo) {
|
||||||
|
testUpdater.lock.Lock()
|
||||||
|
defer testUpdater.lock.Unlock()
|
||||||
|
|
||||||
|
testUpdater.releases = releases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader) (updater.VersionInfo, error) {
|
||||||
|
testUpdater.lock.RLock()
|
||||||
|
defer testUpdater.lock.RUnlock()
|
||||||
|
|
||||||
|
return testUpdater.releases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.Release) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/ProtonMail/proton-bridge/v3/internal/async (interfaces: PanicHandler)
|
// Source: github.com/ProtonMail/gluon/async (interfaces: PanicHandler)
|
||||||
|
|
||||||
// Package mocks is a generated GoMock package.
|
// Package mocks is a generated GoMock package.
|
||||||
package mocks
|
package mocks
|
||||||
@ -34,13 +34,13 @@ func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandlePanic mocks base method.
|
// HandlePanic mocks base method.
|
||||||
func (m *MockPanicHandler) HandlePanic() {
|
func (m *MockPanicHandler) HandlePanic(arg0 interface{}) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
m.ctrl.Call(m, "HandlePanic")
|
m.ctrl.Call(m, "HandlePanic", arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandlePanic indicates an expected call of HandlePanic.
|
// HandlePanic indicates an expected call of HandlePanic.
|
||||||
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
|
func (mr *MockPanicHandlerMockRecorder) HandlePanic(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic), arg0)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
49
internal/bridge/mocks/observability_mocks.go
Normal file
49
internal/bridge/mocks/observability_mocks.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockObservabilitySender struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockObservabilitySenderRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockObservabilitySenderRecorder struct {
|
||||||
|
mock *MockObservabilitySender
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySender {
|
||||||
|
mock := &MockObservabilitySender{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockObservabilitySenderRecorder{mock: mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
|
||||||
|
|
||||||
|
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "AddDistinctMetrics", errType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "AddMetrics", metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
|
||||||
|
"AddDistinctMetrics",
|
||||||
|
reflect.TypeOf((*MockObservabilitySender)(nil).AddDistinctMetrics),
|
||||||
|
errType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.ObservabilityMetric) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
|
||||||
|
}
|
||||||
107
internal/bridge/mocks/telemetry_mocks.go
Normal file
107
internal/bridge/mocks/telemetry_mocks.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/ProtonMail/proton-bridge/v3/internal/telemetry (interfaces: HeartbeatManager)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
reflect "reflect"
|
||||||
|
time "time"
|
||||||
|
|
||||||
|
telemetry "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockHeartbeatManager is a mock of HeartbeatManager interface.
|
||||||
|
type MockHeartbeatManager struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockHeartbeatManagerMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockHeartbeatManagerMockRecorder is the mock recorder for MockHeartbeatManager.
|
||||||
|
type MockHeartbeatManagerMockRecorder struct {
|
||||||
|
mock *MockHeartbeatManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockHeartbeatManager creates a new mock instance.
|
||||||
|
func NewMockHeartbeatManager(ctrl *gomock.Controller) *MockHeartbeatManager {
|
||||||
|
mock := &MockHeartbeatManager{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockHeartbeatManagerMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeartbeatPeriodicInterval mocks base method.
|
||||||
|
func (m *MockHeartbeatManager) GetHeartbeatPeriodicInterval() time.Duration {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetHeartbeatPeriodicInterval")
|
||||||
|
ret0, _ := ret[0].(time.Duration)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeartbeatPeriodicInterval indicates an expected call of GetHeartbeatPeriodicInterval.
|
||||||
|
func (mr *MockHeartbeatManagerMockRecorder) GetHeartbeatPeriodicInterval() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeartbeatPeriodicInterval", reflect.TypeOf((*MockHeartbeatManager)(nil).GetHeartbeatPeriodicInterval))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastHeartbeatSent mocks base method.
|
||||||
|
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetLastHeartbeatSent")
|
||||||
|
ret0, _ := ret[0].(time.Time)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastHeartbeatSent indicates an expected call of GetLastHeartbeatSent.
|
||||||
|
func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).GetLastHeartbeatSent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTelemetryAvailable mocks base method.
|
||||||
|
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
|
||||||
|
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendHeartbeat mocks base method.
|
||||||
|
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendHeartbeat indicates an expected call of SendHeartbeat.
|
||||||
|
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLastHeartbeatSent mocks base method.
|
||||||
|
func (m *MockHeartbeatManager) SetLastHeartbeatSent(arg0 time.Time) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SetLastHeartbeatSent", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLastHeartbeatSent indicates an expected call of SetLastHeartbeatSent.
|
||||||
|
func (mr *MockHeartbeatManagerMockRecorder) SetLastHeartbeatSent(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).SetLastHeartbeatSent), arg0)
|
||||||
|
}
|
||||||
164
internal/bridge/observability_test.go
Normal file
164
internal/bridge/observability_test.go
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBridge_Observability(t *testing.T) {
|
||||||
|
testMetric := proton.ObservabilityMetric{
|
||||||
|
Name: "test1",
|
||||||
|
Version: 1,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
throttlePeriod := time.Millisecond * 500
|
||||||
|
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
|
||||||
|
bridge.PushObservabilityMetric(testMetric)
|
||||||
|
time.Sleep(time.Millisecond * 50) // Wait for the metric to be sent
|
||||||
|
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
time.Sleep(time.Millisecond * 5) // Minor delay between each so our tests aren't flaky
|
||||||
|
bridge.PushObservabilityMetric(testMetric)
|
||||||
|
}
|
||||||
|
// We should still have only 1 metric sent as the throttleDuration has not passed
|
||||||
|
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
// Wait for throttle duration to pass; we should have our remaining metrics posted
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
require.Equal(t, 11, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
// Wait for the throttle duration to reset; i.e. so we have enough time to send a request immediately
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
bridge.PushObservabilityMetric(testMetric)
|
||||||
|
}
|
||||||
|
// We should only have one additional metric sent immediately
|
||||||
|
require.Equal(t, 12, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
// Wait for the others to be sent
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
require.Equal(t, 21, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
// Spam the endpoint a bit
|
||||||
|
for i := 0; i < 300; i++ {
|
||||||
|
if i < 200 {
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
|
}
|
||||||
|
bridge.PushObservabilityMetric(testMetric)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we've sent all metrics
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
|
||||||
|
observabilityStats := s.GetObservabilityStatistics()
|
||||||
|
require.Equal(t, 321, len(observabilityStats.Metrics))
|
||||||
|
|
||||||
|
// Verify that each request had a throttleDuration time difference between each request
|
||||||
|
for i := 0; i < len(observabilityStats.RequestTime)-1; i++ {
|
||||||
|
tOne := observabilityStats.RequestTime[i]
|
||||||
|
tTwo := observabilityStats.RequestTime[i+1]
|
||||||
|
require.True(t, tTwo.Sub(tOne).Abs() > throttlePeriod)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_Observability_Heartbeat(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
throttlePeriod := time.Millisecond * 300
|
||||||
|
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
|
||||||
|
|
||||||
|
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 150)
|
||||||
|
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 350)
|
||||||
|
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 350)
|
||||||
|
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_Observability_UserMetric(t *testing.T) {
|
||||||
|
testMetric := proton.ObservabilityMetric{
|
||||||
|
Name: "test1",
|
||||||
|
Version: 1,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
userMetricPeriod := time.Millisecond * 200
|
||||||
|
heartbeatPeriod := time.Second * 10
|
||||||
|
throttlePeriod := time.Millisecond * 100
|
||||||
|
observability.ModifyUserMetricInterval(userMetricPeriod)
|
||||||
|
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
|
||||||
|
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
|
||||||
|
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
|
||||||
|
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// Two metric updates should be sent again.
|
||||||
|
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// Only a single one should be sent.
|
||||||
|
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -28,7 +28,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/bradenaw/juniper/iterator"
|
"github.com/bradenaw/juniper/iterator"
|
||||||
"github.com/emersion/go-imap/client"
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@ -57,6 +56,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var uidValidities = make(map[string]uint32, len(names))
|
||||||
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
@ -65,7 +65,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
@ -73,7 +73,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
status, err := client.Select("Folders/"+name, false)
|
status, err := client.Select("Folders/"+name, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uint32(1000), status.UidValidity)
|
uidValidities[name] = status.UidValidity
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -84,6 +84,11 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
|
// Wait for refresh event first
|
||||||
|
refreshCh, refreshChDone := chToType[events.Event, events.UserRefreshed](b.GetEvents(events.UserRefreshed{}))
|
||||||
|
defer refreshChDone()
|
||||||
|
require.Equal(t, userID, (<-refreshCh).UserID)
|
||||||
|
// Then sync event
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -98,7 +103,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
@ -106,7 +111,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
status, err := client.Select("Folders/"+name, false)
|
status, err := client.Select("Folders/"+name, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uint32(1001), status.UidValidity)
|
require.Greater(t, status.UidValidity, uidValidities[name])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,10 +18,12 @@
|
|||||||
package bridge_test
|
package bridge_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -30,8 +32,9 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
"github.com/emersion/go-imap/client"
|
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -42,7 +45,7 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
_, _, err := s.CreateUser("recipient", password)
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -88,19 +91,19 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect the sender IMAP client.
|
// Connect the sender IMAP client.
|
||||||
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||||
defer senderIMAPClient.Logout() //nolint:errcheck
|
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
// Connect the recipient IMAP client.
|
// Connect the recipient IMAP client.
|
||||||
recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
// Sender should have 10 messages in the sent folder.
|
// Sender should have 10 messages in the sent folder.
|
||||||
// Recipient should have 0 messages in inbox.
|
// Recipient should have 10 messages in inbox.
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -113,3 +116,622 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendDraftFlags(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a recipient user.
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The sender should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the bridge.
|
||||||
|
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Get the sender user info.
|
||||||
|
userInfo, err := bridge.QueryUserInfo(username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||||
|
defer imapClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// The message to send.
|
||||||
|
message := fmt.Sprintf("From: %v\r\nDate: 01 Jan 1980 00:00:00 +0000\r\nSubject: Test\r\n\r\nHello world!", userInfo.Addresses[0])
|
||||||
|
|
||||||
|
// Save a draft.
|
||||||
|
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), strings.NewReader(message)))
|
||||||
|
|
||||||
|
// Assert that the draft exists and is marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the SMTP client.
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL PLAIN.
|
||||||
|
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
string(userInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, smtpClient.SendMail(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
[]string{"recipient@" + s.GetDomain()},
|
||||||
|
strings.NewReader(message),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Delete the draft: add the \Deleted flag and expunge.
|
||||||
|
{
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), status.Messages)
|
||||||
|
|
||||||
|
// Add the \Deleted flag.
|
||||||
|
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
|
||||||
|
|
||||||
|
// Expunge.
|
||||||
|
require.NoError(t, imapClient.Expunge(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the draft is eventually gone.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 0
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is eventually in the sent folder.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(messages) == 1
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is not marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendInvite(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a recipient user.
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set "attach public keys" to true for the user.
|
||||||
|
withClient(ctx, t, s, username, password, func(ctx context.Context, client *proton.Client) {
|
||||||
|
settings, err := client.SetAttachPublicKey(ctx, proton.SetAttachPublicKeyReq{AttachPublicKey: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, proton.Bool(true), settings.AttachPublicKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The sender should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the bridge.
|
||||||
|
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Get the sender user info.
|
||||||
|
userInfo, err := bridge.QueryUserInfo(username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||||
|
defer imapClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// The message to send.
|
||||||
|
b, err := os.ReadFile("testdata/invite.eml")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Save a draft.
|
||||||
|
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), bytes.NewReader(b)))
|
||||||
|
|
||||||
|
// Assert that the draft exists and is marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the SMTP client.
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL PLAIN.
|
||||||
|
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
string(userInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, smtpClient.SendMail(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
[]string{"recipient@" + s.GetDomain()},
|
||||||
|
bytes.NewReader(b),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Delete the draft: add the \Deleted flag and expunge.
|
||||||
|
{
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), status.Messages)
|
||||||
|
|
||||||
|
// Add the \Deleted flag.
|
||||||
|
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
|
||||||
|
|
||||||
|
// Expunge.
|
||||||
|
require.NoError(t, imapClient.Expunge(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the draft is eventually gone.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 0
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is eventually in the sent folder.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(messages) == 1
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is not marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
||||||
|
// NOTE: Prior to GODT-2887, these tests had inline images, however after the implementation to support
|
||||||
|
// inline images new parts are injected to reference inline images without content-id set. The images
|
||||||
|
// in this test have been changed to regular attachments to keep the original checks in place.
|
||||||
|
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: attachment;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageMultipartWithText = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part2
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: attachment;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/html;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageWithTextOnly = `Content-Type: text/plain;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Subject: A new message Part3
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageMultipartWithoutTextWithTextAttachment = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part4
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/plain; charset=UTF-8; name="text.txt"
|
||||||
|
Content-Disposition: attachment; filename="text.txt"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQK
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
messages := []string{
|
||||||
|
messageMultipartWithoutText,
|
||||||
|
messageMultipartWithText,
|
||||||
|
messageWithTextOnly,
|
||||||
|
messageMultipartWithoutTextWithTextAttachment,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range messages {
|
||||||
|
// Dial the server.
|
||||||
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL LOGIN.
|
||||||
|
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
string(senderInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, client.SendMail(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
[]string{recipientInfo.Addresses[0]},
|
||||||
|
strings.NewReader(m),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||||
|
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// Connect the recipient IMAP client.
|
||||||
|
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||||
|
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if len(messages) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages may not be in order
|
||||||
|
for _, message := range messages {
|
||||||
|
switch {
|
||||||
|
case message.Envelope.Subject == "A new message":
|
||||||
|
// The message that was sent should now include an empty text/plain body part since there was none
|
||||||
|
// in the original message.
|
||||||
|
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||||
|
|
||||||
|
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
|
||||||
|
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
|
||||||
|
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
|
||||||
|
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
|
||||||
|
|
||||||
|
case message.Envelope.Subject == "A new message Part2":
|
||||||
|
// This message already has a text body, should be unchanged
|
||||||
|
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||||
|
|
||||||
|
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
|
||||||
|
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
|
||||||
|
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "html", message.BodyStructure.Parts[0].MIMESubType)
|
||||||
|
|
||||||
|
case message.Envelope.Subject == "A new message Part3":
|
||||||
|
// This message already has a text body, should be unchanged
|
||||||
|
require.Equal(t, 0, len(message.BodyStructure.Parts))
|
||||||
|
|
||||||
|
require.Equal(t, "text", message.BodyStructure.MIMEType)
|
||||||
|
require.Equal(t, "plain", message.BodyStructure.MIMESubType)
|
||||||
|
|
||||||
|
case message.Envelope.Subject == "A new message Part4":
|
||||||
|
// The message that was sent should now include an empty text/plain body part since even though
|
||||||
|
// there was only a text/plain attachment in the original message.
|
||||||
|
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||||
|
|
||||||
|
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
|
||||||
|
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
|
||||||
|
require.Equal(t, "text", message.BodyStructure.Parts[1].MIMEType)
|
||||||
|
require.Equal(t, "plain", message.BodyStructure.Parts[1].MIMESubType)
|
||||||
|
require.Equal(t, "attachment", message.BodyStructure.Parts[1].Disposition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendInlineImage(t *testing.T) {
|
||||||
|
const messageInlineImageOnly = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageInlineImageWithHTML = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part2
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/html;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageInlineImageWithText = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part3
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/plain;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
|
||||||
|
const messageInlineImageFollowedByText = `Content-Type: multipart/mixed;
|
||||||
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
|
Subject: A new message Part4
|
||||||
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Disposition: inline;
|
||||||
|
filename=Cat_August_2010-4.jpeg
|
||||||
|
Content-Type: image/jpeg;
|
||||||
|
name="Cat_August_2010-4.jpeg"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SGVsbG8gd29ybGQ=
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
|
Content-Type: text/plain;charset=utf8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
|
||||||
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||||
|
`
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
messages := []string{
|
||||||
|
messageInlineImageOnly,
|
||||||
|
messageInlineImageWithHTML,
|
||||||
|
messageInlineImageWithText,
|
||||||
|
messageInlineImageFollowedByText,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range messages {
|
||||||
|
// Dial the server.
|
||||||
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL LOGIN.
|
||||||
|
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
string(senderInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, client.SendMail(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
[]string{recipientInfo.Addresses[0]},
|
||||||
|
strings.NewReader(m),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||||
|
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// Connect the recipient IMAP client.
|
||||||
|
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||||
|
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if len(messages) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages may not be in order
|
||||||
|
for _, message := range messages {
|
||||||
|
require.Equal(t, 1, len(message.BodyStructure.Parts))
|
||||||
|
require.Equal(t, "multipart", message.BodyStructure.MIMEType)
|
||||||
|
require.Equal(t, "mixed", message.BodyStructure.MIMESubType)
|
||||||
|
require.Equal(t, "multipart", message.BodyStructure.Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "related", message.BodyStructure.Parts[0].MIMESubType)
|
||||||
|
require.Len(t, message.BodyStructure.Parts[0].Parts, 2)
|
||||||
|
require.Equal(t, "text", message.BodyStructure.Parts[0].Parts[0].MIMEType)
|
||||||
|
require.Equal(t, "html", message.BodyStructure.Parts[0].Parts[0].MIMESubType)
|
||||||
|
require.Equal(t, "image", message.BodyStructure.Parts[0].Parts[1].MIMEType)
|
||||||
|
require.Equal(t, "jpeg", message.BodyStructure.Parts[0].Parts[1].MIMESubType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendAddressDisabled(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
recipientUserID, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderUserID, addrID, err := s.CreateUser("sender", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Dial the server.
|
||||||
|
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
string(senderInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
err = client.SendMail(
|
||||||
|
senderInfo.Addresses[0],
|
||||||
|
[]string{recipientInfo.Addresses[0]},
|
||||||
|
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
|
||||||
|
)
|
||||||
|
|
||||||
|
smtpErr := smtpservice.NewErrCannotSendFromAddress(senderInfo.Addresses[0])
|
||||||
|
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -29,13 +29,12 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBridge_Report(t *testing.T) {
|
func TestBridge_Report(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -56,12 +55,6 @@ func TestBridge_Report(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() { require.NoError(t, conn.Close()) }()
|
defer func() { require.NoError(t, conn.Close()) }()
|
||||||
|
|
||||||
// Sending garbage to the IMAP port should cause the bridge to report it.
|
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
|
||||||
gomock.Eq("Failed to parse IMAP command"),
|
|
||||||
gomock.Any(),
|
|
||||||
).Return(nil)
|
|
||||||
|
|
||||||
// Read lines from the IMAP port.
|
// Read lines from the IMAP port.
|
||||||
lineCh := liner.New(conn).Lines(func() error { return nil })
|
lineCh := liner.New(conn).Lines(func() error { return nil })
|
||||||
|
|
||||||
|
|||||||
137
internal/bridge/server_manager_test.go
Normal file
137
internal/bridge/server_manager_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerManager_ServersStartWithBridge(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Logout())
|
||||||
|
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerManager_ServersKeepsRunningfterUserLogsOut(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||||
|
|
||||||
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Logout())
|
||||||
|
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.T) {
|
||||||
|
otherPassword := []byte("bar")
|
||||||
|
otherUser := "foo"
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
require.NoError(t, s.RevokeUser(userIDOther))
|
||||||
|
|
||||||
|
waitForEvent(t, evtCh, events.UserDeauth{})
|
||||||
|
|
||||||
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Logout())
|
||||||
|
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerManager_NetworkLossStopsServers(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
imapWaiter := waitForIMAPServerReady(bridge)
|
||||||
|
defer imapWaiter.Done()
|
||||||
|
|
||||||
|
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||||
|
defer smtpWaiter.Done()
|
||||||
|
|
||||||
|
imapWaiterStop := waitForIMAPServerStopped(bridge)
|
||||||
|
defer imapWaiterStop.Done()
|
||||||
|
|
||||||
|
smtpWaiterStop := waitForSMTPServerStopped(bridge)
|
||||||
|
defer smtpWaiterStop.Done()
|
||||||
|
|
||||||
|
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Logout())
|
||||||
|
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
netCtl.Disable()
|
||||||
|
|
||||||
|
imapWaiterStop.Wait()
|
||||||
|
smtpWaiterStop.Wait()
|
||||||
|
|
||||||
|
netCtl.Enable()
|
||||||
|
|
||||||
|
imapWaiter.Wait()
|
||||||
|
smtpWaiter.Wait()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -20,16 +20,13 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) GetKeychainApp() (string, error) {
|
func (bridge *Bridge) GetKeychainApp() (string, error) {
|
||||||
@ -47,6 +44,8 @@ func (bridge *Bridge) SetKeychainApp(helper string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.heartbeat.SetKeyChainPref(helper)
|
||||||
|
|
||||||
return vault.SetHelper(vaultDir, helper)
|
return vault.SetHelper(vaultDir, helper)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +53,7 @@ func (bridge *Bridge) GetIMAPPort() int {
|
|||||||
return bridge.vault.GetIMAPPort()
|
return bridge.vault.GetIMAPPort()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetIMAPPort(newPort int) error {
|
func (bridge *Bridge) SetIMAPPort(ctx context.Context, newPort int) error {
|
||||||
if newPort == bridge.vault.GetIMAPPort() {
|
if newPort == bridge.vault.GetIMAPPort() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -63,14 +62,16 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.restartIMAP()
|
bridge.heartbeat.SetIMAPPort(newPort)
|
||||||
|
|
||||||
|
return bridge.restartIMAP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetIMAPSSL() bool {
|
func (bridge *Bridge) GetIMAPSSL() bool {
|
||||||
return bridge.vault.GetIMAPSSL()
|
return bridge.vault.GetIMAPSSL()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
|
func (bridge *Bridge) SetIMAPSSL(ctx context.Context, newSSL bool) error {
|
||||||
if newSSL == bridge.vault.GetIMAPSSL() {
|
if newSSL == bridge.vault.GetIMAPSSL() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -79,14 +80,16 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.restartIMAP()
|
bridge.heartbeat.SetIMAPConnectionMode(newSSL)
|
||||||
|
|
||||||
|
return bridge.restartIMAP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetSMTPPort() int {
|
func (bridge *Bridge) GetSMTPPort() int {
|
||||||
return bridge.vault.GetSMTPPort()
|
return bridge.vault.GetSMTPPort()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetSMTPPort(newPort int) error {
|
func (bridge *Bridge) SetSMTPPort(ctx context.Context, newPort int) error {
|
||||||
if newPort == bridge.vault.GetSMTPPort() {
|
if newPort == bridge.vault.GetSMTPPort() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -95,14 +98,16 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.restartSMTP()
|
bridge.heartbeat.SetSMTPPort(newPort)
|
||||||
|
|
||||||
|
return bridge.restartSMTP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetSMTPSSL() bool {
|
func (bridge *Bridge) GetSMTPSSL() bool {
|
||||||
return bridge.vault.GetSMTPSSL()
|
return bridge.vault.GetSMTPSSL()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
func (bridge *Bridge) SetSMTPSSL(ctx context.Context, newSSL bool) error {
|
||||||
if newSSL == bridge.vault.GetSMTPSSL() {
|
if newSSL == bridge.vault.GetSMTPSSL() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -111,67 +116,53 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.restartSMTP()
|
bridge.heartbeat.SetSMTPConnectionMode(newSSL)
|
||||||
|
|
||||||
|
return bridge.restartSMTP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetGluonDir() string {
|
func (bridge *Bridge) GetGluonCacheDir() string {
|
||||||
return bridge.vault.GetGluonDir()
|
return bridge.vault.GetGluonCacheDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetGluonDataDir() (string, error) {
|
||||||
|
return bridge.locator.ProvideGluonDataPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||||
return safe.RLockRet(func() error {
|
bridge.usersLock.RLock()
|
||||||
currentGluonDir := bridge.GetGluonDir()
|
|
||||||
if newGluonDir == currentGluonDir {
|
defer func() {
|
||||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
logPkg.Info("Restarting user event loops")
|
||||||
|
for _, u := range bridge.users {
|
||||||
|
u.ResumeEventLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVolumeName := filepath.VolumeName(currentGluonDir)
|
bridge.usersLock.RUnlock()
|
||||||
newVolumeName := filepath.VolumeName(newGluonDir)
|
}()
|
||||||
|
|
||||||
if currentVolumeName != newVolumeName {
|
type waiter struct {
|
||||||
return fmt.Errorf("it's currently not possible to move the cache between different volumes")
|
w *userevents.EventPollWaiter
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
waiters := make([]waiter, 0, len(bridge.users))
|
||||||
|
|
||||||
|
logPkg.Info("Pausing user event loops for gluon dir change")
|
||||||
|
for id, u := range bridge.users {
|
||||||
|
waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
logPkg.Info("Waiting on user event loop completion")
|
||||||
|
for _, waiter := range waiters {
|
||||||
|
if err := waiter.w.WaitPollFinished(ctx); err != nil {
|
||||||
|
logPkg.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id)
|
||||||
|
return fmt.Errorf("failed on event loop pause: %w", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
logPkg.Info("Changing gluon directory")
|
||||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
|
||||||
}
|
|
||||||
|
|
||||||
if err := moveDir(bridge.GetGluonDir(), newGluonDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to move gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to set new gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
imapServer, err := newIMAPServer(
|
|
||||||
bridge.vault.GetGluonDir(),
|
|
||||||
bridge.curVersion,
|
|
||||||
bridge.tlsConfig,
|
|
||||||
bridge.reporter,
|
|
||||||
bridge.logIMAPClient,
|
|
||||||
bridge.logIMAPServer,
|
|
||||||
bridge.imapEventCh,
|
|
||||||
bridge.tasks,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.imapServer = imapServer
|
|
||||||
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.serveIMAP(); err != nil {
|
|
||||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, bridge.usersLock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||||
@ -185,6 +176,8 @@ func (bridge *Bridge) SetProxyAllowed(allowed bool) error {
|
|||||||
bridge.proxyCtl.DisallowProxy()
|
bridge.proxyCtl.DisallowProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.heartbeat.SetDoh(allowed)
|
||||||
|
|
||||||
return bridge.vault.SetProxyAllowed(allowed)
|
return bridge.vault.SetProxyAllowed(allowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +191,8 @@ func (bridge *Bridge) SetShowAllMail(show bool) error {
|
|||||||
user.SetShowAllMail(show)
|
user.SetShowAllMail(show)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.heartbeat.SetShowAllMail(show)
|
||||||
|
|
||||||
return bridge.vault.SetShowAllMail(show)
|
return bridge.vault.SetShowAllMail(show)
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
@ -211,6 +206,8 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
|
|||||||
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.heartbeat.SetAutoStart(autostart)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -231,6 +228,10 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetUpdateRollout() float64 {
|
||||||
|
return bridge.vault.GetUpdateRollout()
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetAutoUpdate() bool {
|
func (bridge *Bridge) GetAutoUpdate() bool {
|
||||||
return bridge.vault.GetAutoUpdate()
|
return bridge.vault.GetAutoUpdate()
|
||||||
}
|
}
|
||||||
@ -244,11 +245,31 @@ func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.heartbeat.SetAutoUpdate(autoUpdate)
|
||||||
|
|
||||||
bridge.goUpdate()
|
bridge.goUpdate()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetTelemetryDisabled() bool {
|
||||||
|
return bridge.vault.GetTelemetryDisabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
|
||||||
|
if err := bridge.vault.SetTelemetryDisabled(isDisabled); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// If telemetry is re-enabled locally, try to send the heartbeat.
|
||||||
|
if isDisabled {
|
||||||
|
bridge.heartbeat.stop()
|
||||||
|
} else {
|
||||||
|
bridge.heartbeat.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetUpdateChannel() updater.Channel {
|
func (bridge *Bridge) GetUpdateChannel() updater.Channel {
|
||||||
return bridge.vault.GetUpdateChannel()
|
return bridge.vault.GetUpdateChannel()
|
||||||
}
|
}
|
||||||
@ -262,6 +283,8 @@ func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.heartbeat.SetBeta(channel)
|
||||||
|
|
||||||
bridge.goUpdate()
|
bridge.goUpdate()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -272,23 +295,11 @@ func (bridge *Bridge) GetCurrentVersion() *semver.Version {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
||||||
return bridge.vault.GetLastVersion()
|
return bridge.lastVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetFirstStart() bool {
|
func (bridge *Bridge) GetFirstStart() bool {
|
||||||
return bridge.vault.GetFirstStart()
|
return bridge.firstStart
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) SetFirstStart(firstStart bool) error {
|
|
||||||
return bridge.vault.SetFirstStart(firstStart)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) GetFirstStartGUI() bool {
|
|
||||||
return bridge.vault.GetFirstStartGUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) SetFirstStartGUI(firstStart bool) error {
|
|
||||||
return bridge.vault.SetFirstStartGUI(firstStart)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetColorScheme() string {
|
func (bridge *Bridge) GetColorScheme() string {
|
||||||
@ -299,6 +310,13 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
|||||||
return bridge.vault.SetColorScheme(colorScheme)
|
return bridge.vault.SetColorScheme(colorScheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetKnowledgeBaseSuggestions(userInput string) (kb.ArticleList, error) {
|
||||||
|
return kb.GetSuggestions(userInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FactoryReset deletes all users, wipes the vault, and deletes all files.
|
||||||
|
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
|
||||||
|
// which we need at next startup to decrypt the vault.
|
||||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||||
// Delete all the users.
|
// Delete all the users.
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
@ -308,40 +326,15 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
// Wipe the vault.
|
// Wipe the vault.
|
||||||
gluonDir, err := bridge.locator.ProvideGluonPath()
|
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("Failed to provide gluon dir")
|
logPkg.WithError(err).Error("Failed to provide gluon dir")
|
||||||
} else if err := bridge.vault.Reset(gluonDir); err != nil {
|
} else if err := bridge.vault.Reset(gluonCacheDir); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to reset vault")
|
logPkg.WithError(err).Error("Failed to reset vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then delete all files.
|
// Lastly, delete all files except the vault.
|
||||||
if err := bridge.locator.Clear(); err != nil {
|
if err := bridge.locator.Clear(bridge.vault.Path()); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to clear data paths")
|
logPkg.WithError(err).Error("Failed to clear data paths")
|
||||||
}
|
|
||||||
|
|
||||||
// Lastly clear the keychain.
|
|
||||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to get vault dir")
|
|
||||||
} else if helper, err := vault.GetHelper(vaultDir); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to get keychain helper")
|
|
||||||
} else if keychain, err := keychain.NewKeychain(helper, constants.KeyChainName); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to get keychain")
|
|
||||||
} else if err := keychain.Clear(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to clear keychain")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPort(addr net.Addr) int {
|
|
||||||
switch addr := addr.(type) {
|
|
||||||
case *net.TCPAddr:
|
|
||||||
return addr.Port
|
|
||||||
|
|
||||||
case *net.UDPAddr:
|
|
||||||
return addr.Port
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -25,12 +25,13 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBridge_Settings_GluonDir(t *testing.T) {
|
func TestBridge_Settings_GluonDir(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Create a user.
|
// Create a user.
|
||||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -51,13 +52,52 @@ func TestBridge_Settings_GluonDir(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
<-syncCh
|
||||||
|
})
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Create a new location for the Gluon data.
|
||||||
|
newGluonDir := t.TempDir()
|
||||||
|
|
||||||
|
// Move the gluon dir; it should also move the user's data.
|
||||||
|
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
|
||||||
|
|
||||||
|
// Check that the new directory is not empty.
|
||||||
|
entries, err := os.ReadDir(newGluonDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// There should be at least one entry.
|
||||||
|
require.NotEmpty(t, entries)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
curPort := bridge.GetIMAPPort()
|
curPort := bridge.GetIMAPPort()
|
||||||
|
|
||||||
// Set the port to 1144.
|
// Set the port to 1144.
|
||||||
require.NoError(t, bridge.SetIMAPPort(1144))
|
require.NoError(t, bridge.SetIMAPPort(ctx, 1144))
|
||||||
|
|
||||||
// Get the new setting.
|
// Get the new setting.
|
||||||
require.Equal(t, 1144, bridge.GetIMAPPort())
|
require.Equal(t, 1144, bridge.GetIMAPPort())
|
||||||
@ -70,12 +110,12 @@ func TestBridge_Settings_IMAPPort(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// By default, IMAP SSL is disabled.
|
// By default, IMAP SSL is disabled.
|
||||||
require.False(t, bridge.GetIMAPSSL())
|
require.False(t, bridge.GetIMAPSSL())
|
||||||
|
|
||||||
// Enable IMAP SSL.
|
// Enable IMAP SSL.
|
||||||
require.NoError(t, bridge.SetIMAPSSL(true))
|
require.NoError(t, bridge.SetIMAPSSL(ctx, true))
|
||||||
|
|
||||||
// Get the new setting.
|
// Get the new setting.
|
||||||
require.True(t, bridge.GetIMAPSSL())
|
require.True(t, bridge.GetIMAPSSL())
|
||||||
@ -85,11 +125,11 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_Settings_SMTPPort(t *testing.T) {
|
func TestBridge_Settings_SMTPPort(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
curPort := bridge.GetSMTPPort()
|
curPort := bridge.GetSMTPPort()
|
||||||
|
|
||||||
// Set the port to 1024.
|
// Set the port to 1024.
|
||||||
require.NoError(t, bridge.SetSMTPPort(1024))
|
require.NoError(t, bridge.SetSMTPPort(ctx, 1024))
|
||||||
|
|
||||||
// Get the new setting.
|
// Get the new setting.
|
||||||
require.Equal(t, 1024, bridge.GetSMTPPort())
|
require.Equal(t, 1024, bridge.GetSMTPPort())
|
||||||
@ -102,12 +142,12 @@ func TestBridge_Settings_SMTPPort(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_Settings_SMTPSSL(t *testing.T) {
|
func TestBridge_Settings_SMTPSSL(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// By default, SMTP SSL is disabled.
|
// By default, SMTP SSL is disabled.
|
||||||
require.False(t, bridge.GetSMTPSSL())
|
require.False(t, bridge.GetSMTPSSL())
|
||||||
|
|
||||||
// Enable SMTP SSL.
|
// Enable SMTP SSL.
|
||||||
require.NoError(t, bridge.SetSMTPSSL(true))
|
require.NoError(t, bridge.SetSMTPSSL(ctx, true))
|
||||||
|
|
||||||
// Get the new setting.
|
// Get the new setting.
|
||||||
require.True(t, bridge.GetSMTPSSL())
|
require.True(t, bridge.GetSMTPSSL())
|
||||||
@ -158,30 +198,11 @@ func TestBridge_Settings_Autostart(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_Settings_FirstStart(t *testing.T) {
|
func TestBridge_Settings_FirstStart(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// By default, first start is true.
|
// By default, first start is true.
|
||||||
require.True(t, bridge.GetFirstStart())
|
require.True(t, bridge.GetFirstStart())
|
||||||
|
|
||||||
// Set first start to false.
|
// the setting of the first start value is managed by bridge itself, so the setter is not exported.
|
||||||
require.NoError(t, bridge.SetFirstStart(false))
|
|
||||||
|
|
||||||
// Get the new setting.
|
|
||||||
require.False(t, bridge.GetFirstStart())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_Settings_FirstStartGUI(t *testing.T) {
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
// By default, first start is true.
|
|
||||||
require.True(t, bridge.GetFirstStartGUI())
|
|
||||||
|
|
||||||
// Set first start to false.
|
|
||||||
require.NoError(t, bridge.SetFirstStartGUI(false))
|
|
||||||
|
|
||||||
// Get the new setting.
|
|
||||||
require.False(t, bridge.GetFirstStartGUI())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -20,97 +20,38 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
|
||||||
"github.com/emersion/go-sasl"
|
|
||||||
"github.com/emersion/go-smtp"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) serveSMTP() error {
|
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
||||||
logrus.Info("Starting SMTP server")
|
return bridge.serverManager.RestartSMTP(ctx)
|
||||||
|
|
||||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create SMTP listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.smtpListener = smtpListener
|
|
||||||
|
|
||||||
bridge.tasks.Once(func(context.Context) {
|
|
||||||
if err := bridge.smtpServer.Serve(smtpListener); err != nil {
|
|
||||||
logrus.WithError(err).Info("SMTP server stopped")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
|
||||||
return fmt.Errorf("failed to store SMTP port in vault: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) restartSMTP() error {
|
type bridgeSMTPSettings struct {
|
||||||
logrus.Info("Restarting SMTP server")
|
b *Bridge
|
||||||
|
|
||||||
if err := bridge.closeSMTP(); err != nil {
|
|
||||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
|
||||||
|
|
||||||
return bridge.serveSMTP()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We close the listener ourselves even though it's also closed by smtpServer.Close().
|
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
|
||||||
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
|
return b.b.tlsConfig
|
||||||
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
|
|
||||||
// even after the server has been closed. So we close the listener ourselves to unblock it.
|
|
||||||
func (bridge *Bridge) closeSMTP() error {
|
|
||||||
logrus.Info("Closing SMTP server")
|
|
||||||
|
|
||||||
if bridge.smtpListener != nil {
|
|
||||||
if err := bridge.smtpListener.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to close SMTP listener: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.smtpServer.Close(); err != nil {
|
|
||||||
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
|
func (b *bridgeSMTPSettings) Log() bool {
|
||||||
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
|
return b.b.logSMTP
|
||||||
|
}
|
||||||
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
|
|
||||||
|
func (b *bridgeSMTPSettings) Port() int {
|
||||||
smtpServer.TLSConfig = tlsConfig
|
return b.b.vault.GetSMTPPort()
|
||||||
smtpServer.Domain = constants.Host
|
}
|
||||||
smtpServer.AllowInsecureAuth = true
|
|
||||||
smtpServer.MaxLineLength = 1 << 16
|
func (b *bridgeSMTPSettings) SetPort(i int) error {
|
||||||
smtpServer.ErrorLog = logging.NewSMTPLogger()
|
return b.b.vault.SetSMTPPort(i)
|
||||||
|
}
|
||||||
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
|
|
||||||
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
|
func (b *bridgeSMTPSettings) UseSSL() bool {
|
||||||
return sasl.NewLoginServer(func(username, password string) error {
|
return b.b.vault.GetSMTPSSL()
|
||||||
return conn.Session().AuthPlain(username, password)
|
}
|
||||||
})
|
|
||||||
})
|
func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
|
||||||
|
return &bridgeUserAgentUpdater{Bridge: b.b}
|
||||||
if logSMTP {
|
|
||||||
log := logrus.WithField("protocol", "SMTP")
|
|
||||||
log.Warning("================================================")
|
|
||||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
|
||||||
log.Warning("================================================")
|
|
||||||
|
|
||||||
smtpServer.Debug = logging.NewSMTPDebugLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
return smtpServer
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
|
||||||
"github.com/emersion/go-smtp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type smtpBackend struct {
|
|
||||||
*Bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
type smtpSession struct {
|
|
||||||
*Bridge
|
|
||||||
|
|
||||||
userID string
|
|
||||||
authID string
|
|
||||||
|
|
||||||
from string
|
|
||||||
to []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (be *smtpBackend) NewSession(*smtp.Conn) (smtp.Session, error) {
|
|
||||||
return &smtpSession{Bridge: be.Bridge}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smtpSession) AuthPlain(username, password string) error {
|
|
||||||
return safe.RLockRet(func() error {
|
|
||||||
for _, user := range s.users {
|
|
||||||
addrID, err := user.CheckAuth(username, []byte(password))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.userID = user.ID()
|
|
||||||
s.authID = addrID
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("invalid username or password")
|
|
||||||
}, s.usersLock)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smtpSession) Reset() {
|
|
||||||
s.from = ""
|
|
||||||
s.to = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smtpSession) Logout() error {
|
|
||||||
s.Reset()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
|
|
||||||
s.from = from
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smtpSession) Rcpt(to string) error {
|
|
||||||
if len(to) > 0 {
|
|
||||||
s.to = append(s.to, to)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smtpSession) Data(r io.Reader) error {
|
|
||||||
return safe.RLockRet(func() error {
|
|
||||||
user, ok := s.users[s.userID]
|
|
||||||
if !ok {
|
|
||||||
return ErrNoSuchUser
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.SendMail(s.authID, s.from, s.to, r)
|
|
||||||
}, s.usersLock)
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,19 +21,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/bradenaw/juniper/iterator"
|
"github.com/bradenaw/juniper/iterator"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
@ -79,7 +83,7 @@ func TestBridge_Sync(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
@ -111,15 +115,6 @@ func TestBridge_Sync(t *testing.T) {
|
|||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
|
||||||
defer func() { _ = client.Logout() }()
|
|
||||||
|
|
||||||
status, err := client.Select(`Folders/folder`, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Less(t, status.Messages, uint32(numMsg))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the network limit, allowing the sync to finish.
|
// Remove the network limit, allowing the sync to finish.
|
||||||
@ -135,7 +130,7 @@ func TestBridge_Sync(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
@ -186,7 +181,7 @@ func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
@ -237,7 +232,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
var total uint64
|
var total uint64
|
||||||
|
|
||||||
// The initial user should be fully synced.
|
// The initial user should be fully synced.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -251,7 +246,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Now let's remove the user and stop the network at 2/3 of the data.
|
// Now let's remove the user and stop the network at 2/3 of the data.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -259,28 +254,22 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
netCtl.SetReadLimit(2 * total / 3)
|
netCtl.SetReadLimit(2 * total / 3)
|
||||||
|
|
||||||
// Login the user; its sync should fail.
|
// Login the user; its sync should fail.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
{
|
{
|
||||||
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
syncFailedCh, syncFailedDone := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
||||||
defer done()
|
defer syncFailedDone()
|
||||||
|
|
||||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncFailedCh).UserID)
|
||||||
|
|
||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
|
||||||
defer func() { _ = client.Logout() }()
|
|
||||||
|
|
||||||
status, err := client.Select(`Folders/folder`, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Less(t, status.Messages, uint32(numMsg))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new mailbox and move that last 1/3 of the messages into it to simulate user
|
// Create a new mailbox and move that last 1/3 of the messages into it to simulate user
|
||||||
@ -299,28 +288,18 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
|
|
||||||
// Remove the network limit, allowing the sync to finish.
|
// Remove the network limit, allowing the sync to finish.
|
||||||
netCtl.SetReadLimit(0)
|
netCtl.SetReadLimit(0)
|
||||||
|
|
||||||
{
|
{
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, info.State == bridge.Connected)
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
status, err := client.Select(`Folders/folder`, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// Original folder should have more than 0 messages and less than the total.
|
|
||||||
require.Greater(t, status.Messages, uint32(0))
|
|
||||||
require.Less(t, status.Messages, uint32(numMsg))
|
|
||||||
|
|
||||||
// Check that the new messages arrive in the right location.
|
// Check that the new messages arrive in the right location.
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
status, err := client.Select(`Folders/folder2`, true)
|
status, err := client.Select(`Folders/folder2`, true)
|
||||||
@ -338,6 +317,379 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
}, server.WithTLS(false))
|
}, server.WithTLS(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
|
||||||
|
numMsg := 1 << 8
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
||||||
|
defer addressCreatedDone()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
// Create a new address
|
||||||
|
newAddress := "foo@proton.ch"
|
||||||
|
addrID, err := s.CreateAddress(userID, newAddress, password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
event := <-addressCreatedCh
|
||||||
|
require.Equal(t, userID, event.UserID)
|
||||||
|
require.Equal(t, newAddress, event.Email)
|
||||||
|
require.Equal(t, addrID, event.AddressID)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_RefreshDuringSyncRestartSync(t *testing.T) {
|
||||||
|
numMsg := 1 << 8
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
var refreshPerformed atomic.Bool
|
||||||
|
refreshPerformed.Store(false)
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
if !refreshPerformed.Load() {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
require.NoError(t, err, s.RefreshUser(userID, proton.RefreshMail))
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
refreshPerformed.Store(true)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
|
||||||
|
numMsg := 1 << 8
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var allowSyncToProgress atomic.Bool
|
||||||
|
allowSyncToProgress.Store(false)
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
if !allowSyncToProgress.Load() {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
||||||
|
defer addressCreatedDone()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
// create 20 more messages and move them to inbox
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
// User AddrID2 event as a check point to see when the new address was created.
|
||||||
|
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
allowSyncToProgress.Store(true)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
|
// At most two events can be published, one for the first address, then for the second.
|
||||||
|
// if the second event is not `addrID2` then something went wrong.
|
||||||
|
event := <-addressCreatedCh
|
||||||
|
if event.AddressID == addrID1 {
|
||||||
|
event = <-addressCreatedCh
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, addrID2, event.AddressID)
|
||||||
|
|
||||||
|
info, err := bridge.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
// Finally check if the 20 messages are in INBOX.
|
||||||
|
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(20), status.Messages)
|
||||||
|
|
||||||
|
// Finally check if the numMsg are in the folder.
|
||||||
|
status, err = client.Status("Folders/folder", []imap.StatusItem{imap.StatusMessages})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(numMsg), status.Messages)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_MessageCreateDuringSync(t *testing.T) {
|
||||||
|
numMsg := 1 << 8
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
var allowSyncToProgress atomic.Bool
|
||||||
|
allowSyncToProgress.Store(false)
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
if !allowSyncToProgress.Load() {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
||||||
|
defer addressCreatedDone()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
// create 20 more messages and move them to inbox
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
// User AddrID2 event as a check point to see when the new address was created.
|
||||||
|
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// At most two events can be published, one for the first address, then for the second.
|
||||||
|
// if the second event is not `addrID` then something went wrong.
|
||||||
|
event := <-addressCreatedCh
|
||||||
|
require.Equal(t, addrID, event.AddressID)
|
||||||
|
allowSyncToProgress.Store(true)
|
||||||
|
|
||||||
|
info, err := bridge.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
// Finally check if the 20 messages are in INBOX.
|
||||||
|
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return uint32(20) == status.Messages
|
||||||
|
}, 10*time.Second, time.Second)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for sync to finish
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
settingsPath, err := locator.ProvideSettingsPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
|
||||||
|
// Check sync state is complete
|
||||||
|
{
|
||||||
|
state, err := imapservice.NewSyncState(syncStatePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, syncStatus.IsComplete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// corrupt the vault
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
|
||||||
|
|
||||||
|
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check sync state is reset.
|
||||||
|
{
|
||||||
|
state, err := imapservice.NewSyncState(syncStatePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, syncStatus.IsComplete())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_AddressOrderChangeDuringSyncInCombinedModeDoesNotTriggerBadEventOnNewMessage(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userInfoChanged, done := chToType[events.Event, events.UserChanged](bridge.GetEvents(events.UserChanged{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
info, err := bridge.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(info.Addresses))
|
||||||
|
require.Equal(t, info.Addresses[0], "user@proton.local")
|
||||||
|
|
||||||
|
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID}))
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID2, proton.InboxLabel, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Since we can't intercept events at this time, we sleep for a bit to make sure the
|
||||||
|
// new message does not get combined into the event below. This ensures the newly created
|
||||||
|
// goes through the full code flow which triggered the original bad event.
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
require.NoError(t, s.SetAddressOrder(userID, []string{addrID, addrID2}))
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case e := <-userInfoChanged:
|
||||||
|
require.Equal(t, userID, e.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
||||||
m := proton.New(
|
m := proton.New(
|
||||||
proton.WithHostURL(s.GetHostURL()),
|
proton.WithHostURL(s.GetHostURL()),
|
||||||
@ -351,7 +703,7 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
|
|||||||
fn(ctx, c)
|
fn(ctx, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) { //nolint:unused
|
func clientFetch(client *client.Client, mailbox string, extraItems ...imap.FetchItem) ([]*imap.Message, error) {
|
||||||
status, err := client.Select(mailbox, false)
|
status, err := client.Select(mailbox, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -363,10 +715,13 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
|
|||||||
|
|
||||||
resCh := make(chan *imap.Message)
|
resCh := make(chan *imap.Message)
|
||||||
|
|
||||||
|
fetchItems := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure, "BODY.PEEK[]"}
|
||||||
|
fetchItems = append(fetchItems, extraItems...)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := client.Fetch(
|
if err := client.Fetch(
|
||||||
&imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}},
|
&imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}},
|
||||||
[]imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"},
|
fetchItems,
|
||||||
resCh,
|
resCh,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -376,6 +731,35 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
|
|||||||
return iterator.Collect(iterator.Chan(resCh)), nil
|
return iterator.Collect(iterator.Chan(resCh)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clientStore(client *client.Client, from, to int, isUID bool, item imap.StoreItem, flags ...string) error {
|
||||||
|
var storeFunc func(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error
|
||||||
|
|
||||||
|
if isUID {
|
||||||
|
storeFunc = client.UidStore
|
||||||
|
} else {
|
||||||
|
storeFunc = client.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
return storeFunc(
|
||||||
|
&imap.SeqSet{Set: []imap.Seq{{Start: uint32(from), Stop: uint32(to)}}},
|
||||||
|
item,
|
||||||
|
xslices.Map(flags, func(flag string) interface{} { return flag }),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientList(client *client.Client) []*imap.MailboxInfo {
|
||||||
|
resCh := make(chan *imap.MailboxInfo)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := client.List("", "*", resCh); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return iterator.Collect(iterator.Chan(resCh))
|
||||||
|
}
|
||||||
|
|
||||||
func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, count int) []string {
|
func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, count int) []string {
|
||||||
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
|
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -384,6 +768,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
|
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
|
||||||
|
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
|
||||||
user, err := c.GetUser(ctx)
|
user, err := c.GetUser(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -396,10 +784,20 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||||||
keyPass, err := salt.SaltForKey(password, user.Keys.Primary().ID)
|
keyPass, err := salt.SaltForKey(password, user.Keys.Primary().ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
_, addrKRs, err := proton.Unlock(user, addr, keyPass, async.NoopPanicHandler{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
res, err := stream.Collect(ctx, c.ImportMessages(
|
_, ok := addrKRs[addrID]
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
var msgFlags proton.MessageFlag
|
||||||
|
if flags == 0 {
|
||||||
|
msgFlags = proton.MessageFlagReceived
|
||||||
|
} else {
|
||||||
|
msgFlags = flags
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := c.ImportMessages(
|
||||||
ctx,
|
ctx,
|
||||||
addrKRs[addrID],
|
addrKRs[addrID],
|
||||||
runtime.NumCPU(),
|
runtime.NumCPU(),
|
||||||
@ -409,12 +807,15 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||||||
Metadata: proton.ImportMetadata{
|
Metadata: proton.ImportMetadata{
|
||||||
AddressID: addrID,
|
AddressID: addrID,
|
||||||
LabelIDs: []string{labelID},
|
LabelIDs: []string{labelID},
|
||||||
Flags: proton.MessageFlagReceived,
|
Flags: msgFlags,
|
||||||
},
|
},
|
||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
})...,
|
})...,
|
||||||
))
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := stream.Collect(ctx, str)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return xslices.Map(res, func(res proton.ImportRes) string {
|
return xslices.Map(res, func(res proton.ImportRes) string {
|
||||||
|
|||||||
82
internal/bridge/sync_unix_test.go
Normal file
82
internal/bridge/sync_unix_test.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Disabled due to flakiness.
|
||||||
|
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
||||||
|
var rlimitCurrent syscall.Rlimit
|
||||||
|
|
||||||
|
require.NoError(t, syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
|
||||||
|
|
||||||
|
// Restore RLimit for Process at the end of this test
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
|
||||||
|
}()
|
||||||
|
|
||||||
|
rlimit := syscall.Rlimit{
|
||||||
|
Max: 100,
|
||||||
|
Cur: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimit))
|
||||||
|
|
||||||
|
numMsg := 1 << 8
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := bridge.GetEvents(events.SyncFailed{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
evt := <-syncCh
|
||||||
|
switch e := evt.(type) {
|
||||||
|
case events.SyncFailed:
|
||||||
|
require.Equal(t, userID, e.UserID)
|
||||||
|
default:
|
||||||
|
require.Fail(t, "Expected events.SyncFailed{}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
85
internal/bridge/testdata/invite.eml
vendored
Normal file
85
internal/bridge/testdata/invite.eml
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
From: <username@proton.local>
|
||||||
|
To: <recipient@proton.local>
|
||||||
|
Subject: Testing calendar invite
|
||||||
|
Date: Fri, 3 Feb 2023 01:04:32 +0100
|
||||||
|
Message-ID: <000001d93763$183b74e0$48b25ea0$@proton.local>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/calendar; method=REQUEST;
|
||||||
|
charset="utf-8"
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Mailer: Microsoft Outlook 16.0
|
||||||
|
Thread-Index: Adk3Yw5pLdgwsT46RviXb/nfvQlesQAAAmGA
|
||||||
|
Content-Language: en-gb
|
||||||
|
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:REQUEST
|
||||||
|
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Central European Standard Time
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:16011028T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:16010325T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
END:DAYLIGHT
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;CN=recipient@proton.local;RSVP=TRUE:mailto:recipient@proton.local
|
||||||
|
CLASS:PUBLIC
|
||||||
|
CREATED:20230203T000432Z
|
||||||
|
DESCRIPTION:qweqweqweqweqweqwe/gn\\n
|
||||||
|
DTEND;TZID="Central European Standard Time":20230203T020000
|
||||||
|
DTSTAMP:20230203T000432Z
|
||||||
|
DTSTART;TZID="Central European Standard Time":20230203T013000
|
||||||
|
LAST-MODIFIED:20230203T000432Z
|
||||||
|
LOCATION:qweqwe
|
||||||
|
ORGANIZER;CN=username@proton.local:mailto:username@proton.local
|
||||||
|
PRIORITY:5
|
||||||
|
SEQUENCE:0
|
||||||
|
SUMMARY;LANGUAGE=en-gb:Testing calendar invite
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
UID:040000008200E00074C5B7101A82E008000000003080B2796B37D901000000000000000
|
||||||
|
0100000001236CD1CD93CA9449C6FF1AC4DEAC44E
|
||||||
|
X-ALT-DESC;FMTTYPE=text/html:<html xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-mic
|
||||||
|
rosoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/
|
||||||
|
12/omml" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv=Co
|
||||||
|
ntent-Type content="text/html/g; charset=us-ascii"><meta name=Generator con
|
||||||
|
tent="Microsoft Word 15 (filtered medium)"><style><!--/gn/* Font Definition
|
||||||
|
s *//gn@font-face\\n {font-family:"Cambria Math"\\;\\n panose-1:2 4 5 3 5 4 6
|
||||||
|
3 2 4/g;}\\n@font-face\\n {font-family:Calibri\\;\\n panose-1:2 15 5 2 2 2 4 3
|
||||||
|
2 4/g;}\\n/* Style Definitions */\\np.MsoNormal\\, li.MsoNormal\\, div.MsoNorma
|
||||||
|
l/gn {margin:0cm\\;\\n font-size:11.0pt\\;\\n font-family:"Calibri"\\,sans-serif
|
||||||
|
/g;\\n mso-fareast-language:EN-US\\;}\\nspan.EmailStyle18\\n {mso-style-type:pe
|
||||||
|
rsonal-compose/g;\\n font-family:"Calibri"\\,sans-serif\\;\\n color:windowtext\\
|
||||||
|
;}/gn.MsoChpDefault\\n {mso-style-type:export-only\\;\\n font-size:10.0pt\\;}\\n
|
||||||
|
@page WordSection1/gn {size:612.0pt 792.0pt\\;\\n margin:72.0pt 72.0pt 72.0pt
|
||||||
|
72.0pt/g;}\\ndiv.WordSection1\\n {page:WordSection1\\;}\\n--></style><!--[if g
|
||||||
|
te mso 9]><xml>/gn<o:shapedefaults v:ext="edit" spidmax="1026" />\\n</xml><!
|
||||||
|
[endif]--><!--[if gte mso 9]><xml>/gn<o:shapelayout v:ext="edit">\\n<o:idmap
|
||||||
|
v:ext="edit" data="1" />/gn</o:shapelayout></xml><![endif]--></head><body
|
||||||
|
lang=EN-GB link="#0563C1" vlink="#954F72" style='word-wrap:break-word'><di
|
||||||
|
v class=WordSection1><p class=MsoNormal><span lang=EN-US>qweqweqweqweqweqw
|
||||||
|
e<o:p></o:p></span></p></div></body></html>
|
||||||
|
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
|
||||||
|
X-MICROSOFT-CDO-IMPORTANCE:1
|
||||||
|
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
|
||||||
|
X-MICROSOFT-DISALLOW-COUNTER:FALSE
|
||||||
|
X-MS-OLK-AUTOSTARTCHECK:FALSE
|
||||||
|
X-MS-OLK-CONFTYPE:0
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER:-PT15M
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
|
END:VALARM
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,5 +18,9 @@
|
|||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
|
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
|
||||||
return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey()
|
return bridge.vault.GetBridgeTLSCert()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) SetBridgeTLSCertPath(certPath, keyPath string) error {
|
||||||
|
return bridge.vault.SetBridgeTLSCertPath(certPath, keyPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -26,17 +26,14 @@ import (
|
|||||||
type Locator interface {
|
type Locator interface {
|
||||||
ProvideSettingsPath() (string, error)
|
ProvideSettingsPath() (string, error)
|
||||||
ProvideLogsPath() (string, error)
|
ProvideLogsPath() (string, error)
|
||||||
ProvideGluonPath() (string, error)
|
ProvideGluonCachePath() (string, error)
|
||||||
|
ProvideGluonDataPath() (string, error)
|
||||||
GetLicenseFilePath() string
|
GetLicenseFilePath() string
|
||||||
GetDependencyLicensesLink() string
|
GetDependencyLicensesLink() string
|
||||||
Clear() error
|
Clear(...string) error
|
||||||
}
|
ProvideIMAPSyncConfigPath() (string, error)
|
||||||
|
ProvideUnleashCachePath() (string, error)
|
||||||
type Identifier interface {
|
ProvideNotificationsCachePath() (string, error)
|
||||||
GetUserAgent() string
|
|
||||||
HasClient() bool
|
|
||||||
SetClient(name, version string)
|
|
||||||
SetPlatform(platform string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyController interface {
|
type ProxyController interface {
|
||||||
@ -55,6 +52,9 @@ type Autostarter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Updater interface {
|
type Updater interface {
|
||||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
GetVersionInfoLegacy(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfoLegacy, error)
|
||||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
InstallUpdateLegacy(context.Context, updater.Downloader, updater.VersionInfoLegacy) error
|
||||||
|
RemoveOldUpdates() error
|
||||||
|
GetVersionInfo(context.Context, updater.Downloader) (updater.VersionInfo, error)
|
||||||
|
InstallUpdate(context.Context, updater.Downloader, updater.Release) error
|
||||||
}
|
}
|
||||||
|
|||||||
90
internal/bridge/unleash_test.go
Normal file
90
internal/bridge/unleash_test.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_UnleashService(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Initial startup assumes there is no cached feature flags.
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||||
|
|
||||||
|
s.PushFeatureFlag("test-1")
|
||||||
|
s.PushFeatureFlag("test-2")
|
||||||
|
|
||||||
|
// Wait for poll.
|
||||||
|
time.Sleep(time.Millisecond * 700)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||||
|
|
||||||
|
s.PushFeatureFlag("test-3")
|
||||||
|
time.Sleep(time.Millisecond * 700) // Wait for poll again
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for Bridge to close.
|
||||||
|
time.Sleep(time.Millisecond * 500)
|
||||||
|
|
||||||
|
// Second instance should have a feature flag cache file available. Therefore, all of the flags should evaluate to true on startup.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||||
|
|
||||||
|
s.DeleteFeatureFlags()
|
||||||
|
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 700)
|
||||||
|
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||||
|
|
||||||
|
s.PushFeatureFlag("test-3")
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 700)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||||
|
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,33 +21,168 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
|
"github.com/elastic/go-sysinfo"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) CheckForUpdates() {
|
func (bridge *Bridge) CheckForUpdates() {
|
||||||
bridge.goUpdate()
|
bridge.goUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
|
func (bridge *Bridge) InstallUpdateLegacy(version updater.VersionInfoLegacy) {
|
||||||
log := logrus.WithFields(logrus.Fields{
|
bridge.installChLegacy <- installJobLegacy{version: version, silent: false}
|
||||||
"version": version.Version,
|
}
|
||||||
"current": bridge.curVersion,
|
|
||||||
"channel": bridge.vault.GetUpdateChannel(),
|
|
||||||
})
|
|
||||||
|
|
||||||
select {
|
func (bridge *Bridge) InstallUpdate(release updater.Release) {
|
||||||
case bridge.installCh <- installJob{version: version, silent: false}:
|
bridge.installCh <- installJob{Release: release, Silent: false}
|
||||||
log.Info("The update will be installed manually")
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Info("An update is already being installed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||||
|
updateChannel := bridge.vault.GetUpdateChannel()
|
||||||
|
updateRollout := bridge.vault.GetUpdateRollout()
|
||||||
|
autoUpdateEnabled := bridge.vault.GetAutoUpdate()
|
||||||
|
|
||||||
|
checkSystemVersion := true
|
||||||
|
hostInfo, err := sysinfo.Host()
|
||||||
|
// If we're unable to get host system information we skip the update's minimum/maximum OS version checks
|
||||||
|
if err != nil {
|
||||||
|
checkSystemVersion = false
|
||||||
|
logrus.WithError(err).Error("Failed to obtain host system info while handling updates")
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Failed to obtain host system info while handling updates",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
logrus.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(version.Releases) > 0 {
|
||||||
|
// Update latest is only used to update the release notes and landing page URL
|
||||||
|
bridge.publish(events.UpdateLatest{Release: version.Releases[0]})
|
||||||
|
}
|
||||||
|
|
||||||
|
// minAutoUpdateEvent - used to determine the highest compatible update that satisfies the Minimum Bridge version
|
||||||
|
minAutoUpdateEvent := events.UpdateAvailable{
|
||||||
|
Release: updater.Release{Version: &semver.Version{}},
|
||||||
|
Compatible: false,
|
||||||
|
Silent: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We assume that the version file is always created in descending order
|
||||||
|
// where newer versions are prepended to the top of the releases
|
||||||
|
// The logic for checking update eligibility is as follows:
|
||||||
|
// 1. Check release channel.
|
||||||
|
// 2. Check whether release version is greater.
|
||||||
|
// 3. Check if rollout is larger.
|
||||||
|
// 4. Check OS Version restrictions (provided that restrictions are provided, and we can extract the OS version).
|
||||||
|
// 5. Check Minimum Compatible Bridge Version.
|
||||||
|
// 6. Check if an update package is provided.
|
||||||
|
// 7. Check auto-update.
|
||||||
|
for _, release := range version.Releases {
|
||||||
|
log := logrus.WithFields(logrus.Fields{
|
||||||
|
"current": bridge.curVersion,
|
||||||
|
"channel": updateChannel,
|
||||||
|
"update_version": release.Version,
|
||||||
|
"update_channel": release.ReleaseCategory,
|
||||||
|
"update_min_auto": release.MinAuto,
|
||||||
|
"update_rollout": release.RolloutProportion,
|
||||||
|
"update_min_os_version": release.SystemVersion.Minimum,
|
||||||
|
"update_max_os_version": release.SystemVersion.Maximum,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Debug("Checking update release")
|
||||||
|
|
||||||
|
if !release.ReleaseCategory.UpdateEligible(updateChannel) {
|
||||||
|
log.Debug("Update does not satisfy update channel requirement")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !release.Version.GreaterThan(bridge.curVersion) {
|
||||||
|
log.Debug("Update version is not greater than current version")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if release.RolloutProportion < updateRollout {
|
||||||
|
log.Debug("Update has not been rolled out yet")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkSystemVersion {
|
||||||
|
shouldContinue, err := release.SystemVersion.IsHostVersionEligible(log, hostInfo, bridge.getHostVersion)
|
||||||
|
if err != nil && shouldContinue {
|
||||||
|
log.WithError(err).Error(
|
||||||
|
"Failed to verify host system version compatibility during release check." +
|
||||||
|
"Error is non-fatal continuing with checks",
|
||||||
|
)
|
||||||
|
} else if err != nil {
|
||||||
|
log.WithError(err).Error("Failed to verify host system version compatibility during update check")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldContinue {
|
||||||
|
log.Debug("Host version does not satisfy system requirements for update")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if release.MinAuto != nil && bridge.curVersion.LessThan(release.MinAuto) {
|
||||||
|
log.Debug("Update is available but is incompatible with this Bridge version")
|
||||||
|
if release.Version.GreaterThan(minAutoUpdateEvent.Release.Version) {
|
||||||
|
minAutoUpdateEvent.Release = release
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a provided installer package
|
||||||
|
if found := slices.IndexFunc(release.File, func(file updater.File) bool {
|
||||||
|
return file.Identifier == updater.PackageIdentifier
|
||||||
|
}); found == -1 {
|
||||||
|
log.Error("Update is available but does not contain update package")
|
||||||
|
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Available update does not contain update package",
|
||||||
|
reporter.Context{"update_version": release.Version},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoUpdateEnabled {
|
||||||
|
log.Info("An update is available but auto-update is disabled")
|
||||||
|
bridge.publish(events.UpdateAvailable{
|
||||||
|
Release: release,
|
||||||
|
Compatible: true,
|
||||||
|
Silent: false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've gotten to this point that means an automatic update is available and we should install it
|
||||||
|
safe.RLock(func() {
|
||||||
|
bridge.installCh <- installJob{Release: release, Silent: true}
|
||||||
|
}, bridge.newVersionLock)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a release with a minAuto requirement that we satisfy (alongside all other checks)
|
||||||
|
// then notify the user that a manual update is needed
|
||||||
|
if !minAutoUpdateEvent.Release.Version.Equal(&semver.Version{}) {
|
||||||
|
bridge.publish(minAutoUpdateEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateNotAvailable{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUpdateLegacy(version updater.VersionInfoLegacy) {
|
||||||
log := logrus.WithFields(logrus.Fields{
|
log := logrus.WithFields(logrus.Fields{
|
||||||
"version": version.Version,
|
"version": version.Version,
|
||||||
"current": bridge.curVersion,
|
"current": bridge.curVersion,
|
||||||
@ -55,7 +190,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
bridge.publish(events.UpdateLatest{
|
bridge.publish(events.UpdateLatest{
|
||||||
Version: version,
|
VersionLegacy: version,
|
||||||
})
|
})
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@ -73,43 +208,33 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
|||||||
log.Info("An update is available but is incompatible with this version")
|
log.Info("An update is available but is incompatible with this version")
|
||||||
|
|
||||||
bridge.publish(events.UpdateAvailable{
|
bridge.publish(events.UpdateAvailable{
|
||||||
Version: version,
|
VersionLegacy: version,
|
||||||
Compatible: false,
|
Compatible: false,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
case !bridge.vault.GetAutoUpdate():
|
case !bridge.vault.GetAutoUpdate():
|
||||||
log.Info("An update is available but auto-update is disabled")
|
log.Info("An update is available but auto-update is disabled")
|
||||||
|
|
||||||
bridge.publish(events.UpdateAvailable{
|
bridge.publish(events.UpdateAvailable{
|
||||||
Version: version,
|
VersionLegacy: version,
|
||||||
Compatible: true,
|
Compatible: true,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
safe.RLock(func() {
|
safe.RLock(func() {
|
||||||
if version.Version.GreaterThan(bridge.newVersion) {
|
bridge.installChLegacy <- installJobLegacy{version: version, silent: true}
|
||||||
log.Info("An update is available")
|
|
||||||
|
|
||||||
select {
|
|
||||||
case bridge.installCh <- installJob{version: version, silent: true}:
|
|
||||||
log.Info("The update will be installed silently")
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Info("An update is already being installed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, bridge.newVersionLock)
|
}, bridge.newVersionLock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type installJob struct {
|
type installJobLegacy struct {
|
||||||
version updater.VersionInfo
|
version updater.VersionInfoLegacy
|
||||||
silent bool
|
silent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
func (bridge *Bridge) installUpdateLegacy(ctx context.Context, job installJobLegacy) {
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
log := logrus.WithFields(logrus.Fields{
|
log := logrus.WithFields(logrus.Fields{
|
||||||
"version": job.version.Version,
|
"version": job.version.Version,
|
||||||
@ -117,20 +242,32 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
"channel": bridge.vault.GetUpdateChannel(),
|
"channel": bridge.vault.GetUpdateChannel(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if !job.version.Version.GreaterThan(bridge.newVersion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("silent", job.silent).Info("An update is available")
|
||||||
|
|
||||||
bridge.publish(events.UpdateAvailable{
|
bridge.publish(events.UpdateAvailable{
|
||||||
Version: job.version,
|
VersionLegacy: job.version,
|
||||||
Compatible: true,
|
Compatible: true,
|
||||||
Silent: job.silent,
|
Silent: job.silent,
|
||||||
})
|
})
|
||||||
|
|
||||||
bridge.publish(events.UpdateInstalling{
|
err := bridge.updater.InstallUpdateLegacy(ctx, bridge.api, job.version)
|
||||||
Version: job.version,
|
|
||||||
Silent: job.silent,
|
|
||||||
})
|
|
||||||
|
|
||||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case errors.Is(err, updater.ErrDownloadVerify):
|
||||||
|
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
|
||||||
|
// and we fail silently.
|
||||||
|
log.WithError(err).Error("The update could not be installed, but we will fail silently")
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Cannot download or verify update",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
|
||||||
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||||
log.Info("The update was already installed")
|
log.Info("The update was already installed")
|
||||||
|
|
||||||
@ -138,19 +275,97 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
log.WithError(err).Error("The update could not be installed")
|
log.WithError(err).Error("The update could not be installed")
|
||||||
|
|
||||||
bridge.publish(events.UpdateFailed{
|
bridge.publish(events.UpdateFailed{
|
||||||
Version: job.version,
|
VersionLegacy: job.version,
|
||||||
Silent: job.silent,
|
Silent: job.silent,
|
||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Info("The update was installed successfully")
|
log.Info("The update was installed successfully")
|
||||||
|
|
||||||
bridge.publish(events.UpdateInstalled{
|
bridge.publish(events.UpdateInstalled{
|
||||||
Version: job.version,
|
VersionLegacy: job.version,
|
||||||
Silent: job.silent,
|
Silent: job.silent,
|
||||||
})
|
})
|
||||||
|
|
||||||
bridge.newVersion = job.version.Version
|
bridge.newVersion = job.version.Version
|
||||||
}
|
}
|
||||||
}, bridge.newVersionLock)
|
}, bridge.newVersionLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type installJob struct {
|
||||||
|
Release updater.Release
|
||||||
|
Silent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||||
|
safe.Lock(func() {
|
||||||
|
log := logrus.WithFields(logrus.Fields{
|
||||||
|
"version": job.Release.Version,
|
||||||
|
"current": bridge.curVersion,
|
||||||
|
"channel": bridge.vault.GetUpdateChannel(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if !job.Release.Version.GreaterThan(bridge.newVersion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("silent", job.Silent).Info("An update is available")
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateAvailable{
|
||||||
|
Release: job.Release,
|
||||||
|
Compatible: true,
|
||||||
|
Silent: job.Silent,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.Release)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, updater.ErrReleaseUpdatePackageMissing):
|
||||||
|
log.WithError(err).Error("The update could not be installed but we will fail silently")
|
||||||
|
if reporterErr := bridge.reporter.ReportExceptionWithContext(
|
||||||
|
"Cannot download update, update package is missing",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
case errors.Is(err, updater.ErrDownloadVerify):
|
||||||
|
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
|
||||||
|
// and we fail silently.
|
||||||
|
log.WithError(err).Error("The update could not be installed, but we will fail silently")
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Cannot download or verify update",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
|
||||||
|
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||||
|
log.Info("The update was already installed")
|
||||||
|
|
||||||
|
case err != nil:
|
||||||
|
log.WithError(err).Error("The update could not be installed")
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateFailed{
|
||||||
|
Release: job.Release,
|
||||||
|
Silent: job.Silent,
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Info("The update was installed successfully")
|
||||||
|
|
||||||
|
bridge.publish(events.UpdateInstalled{
|
||||||
|
Release: job.Release,
|
||||||
|
Silent: job.Silent,
|
||||||
|
})
|
||||||
|
|
||||||
|
bridge.newVersion = job.Release.Version
|
||||||
|
}
|
||||||
|
}, bridge.newVersionLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) RemoveOldUpdates() {
|
||||||
|
if err := bridge.updater.RemoveOldUpdates(); err != nil {
|
||||||
|
logrus.WithError(err).Error("Remove old updates fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
700
internal/bridge/updates_test.go
Normal file
700
internal/bridge/updates_test.go
Normal file
@ -0,0 +1,700 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
bridgePkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
|
||||||
|
"github.com/elastic/go-sysinfo/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: we always assume the highest version is always the first in the release json array
|
||||||
|
|
||||||
|
func Test_Update_BetaEligible(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
err := bridge.SetUpdateChannel(updater.EarlyChannel)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.2"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
expectedRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case update := <-updateCh:
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, update)
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for update")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_Stable(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
err := bridge.SetUpdateChannel(updater.StableChannel)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.3"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.4"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_CurrentReleaseNewest(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
err := bridge.SetUpdateChannel(updater.StableChannel)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.1.5"))
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.3"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.4"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_NotRolledOutYet(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.EarlyChannel))
|
||||||
|
bridge.SetCurrentVersionTest(semver.MustParse("2.0.0"))
|
||||||
|
require.NoError(t, bridge.SetRolloutPercentageTest(1.0))
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.5"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 0.5,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.4"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 0.5,
|
||||||
|
MinAuto: &semver.Version{},
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_CheckOSVersion_NoUpdate(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
// Override the OS version check
|
||||||
|
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
|
||||||
|
return "10.0.0"
|
||||||
|
})
|
||||||
|
|
||||||
|
updateNotAvailableCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
updateCh, updateChDone := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer updateChDone()
|
||||||
|
|
||||||
|
expectedRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.4.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "12.0.0",
|
||||||
|
Maximum: "13.0.0",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
expectedRelease,
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "10.1.0",
|
||||||
|
Maximum: "11.5",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateNotAvailableCh)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_CheckOSVersion_HasUpdate(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
// Override the OS version check
|
||||||
|
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
|
||||||
|
return "10.0.0"
|
||||||
|
})
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "10.0.0",
|
||||||
|
Maximum: "10.1.12",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedUpdateReleaseWindowsLinux := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.4.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "12.0.0",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
expectedUpdateReleaseWindowsLinux,
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{
|
||||||
|
Minimum: "11.0.0",
|
||||||
|
},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.1.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedUpdateRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedUpdateReleaseWindowsLinux,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Update_UpdateFromMinVer_UpdateAvailable(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: currentBridgeVersion,
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.1"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.1"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.0"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateInstalled{
|
||||||
|
Release: expectedUpdateRelease,
|
||||||
|
Silent: true,
|
||||||
|
}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual -
|
||||||
|
// if we have an update, but we don't satisfy minVersion, a manual update to the highest possible version should be performed.
|
||||||
|
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.1"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.1"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.0"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.StableReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.1.6"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateAvailable{
|
||||||
|
Release: expectedUpdateRelease,
|
||||||
|
Silent: false,
|
||||||
|
Compatible: false,
|
||||||
|
}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch - only Beta updates are available
|
||||||
|
// nor do we satisfy the minVersion, we can't do anything in this case.
|
||||||
|
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||||
|
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||||
|
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||||
|
|
||||||
|
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||||
|
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||||
|
|
||||||
|
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
expectedUpdateRelease := updater.Release{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.3.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.1"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.1"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.2.0"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||||
|
Version: semver.MustParse("2.2.0"),
|
||||||
|
SystemVersion: versioncompare.SystemVersion{},
|
||||||
|
RolloutProportion: 1.0,
|
||||||
|
MinAuto: semver.MustParse("2.1.6"),
|
||||||
|
File: []updater.File{
|
||||||
|
{
|
||||||
|
URL: "RANDOM_INSTALLER_URL",
|
||||||
|
Identifier: updater.InstallerIdentifier,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "RANDOM_PACKAGE_URL",
|
||||||
|
Identifier: updater.PackageIdentifier,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUpdateRelease,
|
||||||
|
}}
|
||||||
|
|
||||||
|
mocks.Updater.SetLatestVersion(updaterData)
|
||||||
|
|
||||||
|
bridge.CheckForUpdates()
|
||||||
|
|
||||||
|
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -23,19 +23,25 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logUser = logrus.WithField("pkg", "bridge/user") //nolint:gochecknoglobals
|
||||||
|
|
||||||
type UserState int
|
type UserState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -44,6 +50,8 @@ const (
|
|||||||
Connected
|
Connected
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
// UserID is the user's API ID.
|
// UserID is the user's API ID.
|
||||||
UserID string
|
UserID string
|
||||||
@ -64,10 +72,10 @@ type UserInfo struct {
|
|||||||
BridgePass []byte
|
BridgePass []byte
|
||||||
|
|
||||||
// UsedSpace is the amount of space used by the user.
|
// UsedSpace is the amount of space used by the user.
|
||||||
UsedSpace int
|
UsedSpace uint64
|
||||||
|
|
||||||
// MaxSpace is the total amount of space available to the user.
|
// MaxSpace is the total amount of space available to the user.
|
||||||
MaxSpace int
|
MaxSpace uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||||
@ -94,7 +102,7 @@ func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
|||||||
if len(user.AuthUID()) == 0 {
|
if len(user.AuthUID()) == 0 {
|
||||||
state = SignedOut
|
state = SignedOut
|
||||||
}
|
}
|
||||||
info = getUserInfo(user.UserID(), user.Username(), state, user.AddressMode())
|
info = getUserInfo(user.UserID(), user.Username(), user.PrimaryEmail(), state, user.AddressMode())
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
||||||
}
|
}
|
||||||
@ -117,23 +125,28 @@ func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
// LoginAuth begins the login process. It returns an authorized client that might need 2FA.
|
||||||
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*proton.Client, proton.Auth, error) {
|
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte, hvDetails *proton.APIHVDetails) (*proton.Client, proton.Auth, error) {
|
||||||
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
logUser.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
|
||||||
|
|
||||||
if username == "crash@bandicoot" {
|
if username == "crash@bandicoot" {
|
||||||
panic("Your wish is my command.. I crash!")
|
panic("Your wish is my command.. I crash!")
|
||||||
}
|
}
|
||||||
|
client, auth, err := bridge.api.NewClientWithLoginWithHVToken(ctx, username, password, hvDetails)
|
||||||
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if hv.IsHvRequest(err) {
|
||||||
|
logUser.WithFields(logrus.Fields{"username": logging.Sensitive(username),
|
||||||
|
"loginError": err.Error()}).Info("Human Verification requested for login")
|
||||||
|
return nil, proton.Auth{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UserID) }, bridge.usersLock); ok {
|
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UserID) }, bridge.usersLock); ok {
|
||||||
logrus.WithField("userID", auth.UserID).Warn("User already logged in")
|
logUser.WithField("userID", auth.UserID).Warn("User already logged in")
|
||||||
|
|
||||||
if err := client.AuthDelete(ctx); err != nil {
|
if err := client.AuthDelete(ctx); err != nil {
|
||||||
logrus.WithError(err).Warn("Failed to delete auth")
|
logUser.WithError(err).Warn("Failed to delete auth")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, proton.Auth{}, ErrUserAlreadyLoggedIn
|
return nil, proton.Auth{}, ErrUserAlreadyLoggedIn
|
||||||
@ -148,18 +161,23 @@ func (bridge *Bridge) LoginUser(
|
|||||||
client *proton.Client,
|
client *proton.Client,
|
||||||
auth proton.Auth,
|
auth proton.Auth,
|
||||||
keyPass []byte,
|
keyPass []byte,
|
||||||
|
hvDetails *proton.APIHVDetails,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
logrus.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
logUser.WithField("userID", auth.UserID).Info("Logging in authorized user")
|
||||||
|
|
||||||
userID, err := try.CatchVal(
|
userID, err := try.CatchVal(
|
||||||
func() (string, error) {
|
func() (string, error) {
|
||||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass, hvDetails)
|
||||||
},
|
|
||||||
func() error {
|
|
||||||
return client.AuthDelete(ctx)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Failure to unlock will allow retries, so we do not delete auth.
|
||||||
|
if !errors.Is(err, ErrFailedToUnlock) {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logUser.WithError(deleteErr).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
return "", fmt.Errorf("failed to login user: %w", err)
|
return "", fmt.Errorf("failed to login user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,15 +198,16 @@ func (bridge *Bridge) LoginFull(
|
|||||||
getTOTP func() (string, error),
|
getTOTP func() (string, error),
|
||||||
getKeyPass func() ([]byte, error),
|
getKeyPass func() ([]byte, error),
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
logrus.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
logUser.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
|
||||||
|
|
||||||
client, auth, err := bridge.LoginAuth(ctx, username, password)
|
// (atanas) the following may need to be modified once HV is merged (its used only for testing; and depends on whether we will test HV related logic)
|
||||||
|
client, auth, err := bridge.LoginAuth(ctx, username, password, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to begin login process: %w", err)
|
return "", fmt.Errorf("failed to begin login process: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
|
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
|
||||||
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP")
|
logUser.WithField("userID", auth.UserID).Info("Requesting TOTP")
|
||||||
|
|
||||||
totp, err := getTOTP()
|
totp, err := getTOTP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -203,7 +222,7 @@ func (bridge *Bridge) LoginFull(
|
|||||||
var keyPass []byte
|
var keyPass []byte
|
||||||
|
|
||||||
if auth.PasswordMode == proton.TwoPasswordMode {
|
if auth.PasswordMode == proton.TwoPasswordMode {
|
||||||
logrus.WithField("userID", auth.UserID).Info("Requesting mailbox password")
|
logUser.WithField("userID", auth.UserID).Info("Requesting mailbox password")
|
||||||
|
|
||||||
userKeyPass, err := getKeyPass()
|
userKeyPass, err := getKeyPass()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -215,12 +234,21 @@ func (bridge *Bridge) LoginFull(
|
|||||||
keyPass = password
|
keyPass = password
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.LoginUser(ctx, client, auth, keyPass)
|
userID, err := bridge.LoginUser(ctx, client, auth, keyPass, nil)
|
||||||
|
if err != nil {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logUser.WithError(err).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutUser logs out the given user.
|
// LogoutUser logs out the given user.
|
||||||
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||||
logrus.WithField("userID", userID).Info("Logging out user")
|
logUser.WithField("userID", userID).Info("Logging out user")
|
||||||
|
|
||||||
return safe.LockRet(func() error {
|
return safe.LockRet(func() error {
|
||||||
user, ok := bridge.users[userID]
|
user, ok := bridge.users[userID]
|
||||||
@ -240,7 +268,12 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
|||||||
|
|
||||||
// DeleteUser deletes the given user.
|
// DeleteUser deletes the given user.
|
||||||
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||||
logrus.WithField("userID", userID).Info("Deleting user")
|
logUser.WithField("userID", userID).Info("Deleting user")
|
||||||
|
|
||||||
|
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get sync config path")
|
||||||
|
}
|
||||||
|
|
||||||
return safe.LockRet(func() error {
|
return safe.LockRet(func() error {
|
||||||
if !bridge.vault.HasUser(userID) {
|
if !bridge.vault.HasUser(userID) {
|
||||||
@ -251,8 +284,12 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
|||||||
bridge.logoutUser(ctx, user, true, true)
|
bridge.logoutUser(ctx, user, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete use sync config")
|
||||||
|
}
|
||||||
|
|
||||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to delete vault user")
|
logUser.WithError(err).Error("Failed to delete vault user")
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.publish(events.UserDeleted{
|
bridge.publish(events.UserDeleted{
|
||||||
@ -265,7 +302,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
|||||||
|
|
||||||
// SetAddressMode sets the address mode for the given user.
|
// SetAddressMode sets the address mode for the given user.
|
||||||
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
|
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
|
||||||
logrus.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode")
|
logUser.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode")
|
||||||
|
|
||||||
return safe.RLockRet(func() error {
|
return safe.RLockRet(func() error {
|
||||||
user, ok := bridge.users[userID]
|
user, ok := bridge.users[userID]
|
||||||
@ -277,29 +314,63 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
return fmt.Errorf("address mode is already %q", mode)
|
return fmt.Errorf("address mode is already %q", mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetAddressMode(ctx, mode); err != nil {
|
if err := user.SetAddressMode(ctx, mode); err != nil {
|
||||||
return fmt.Errorf("failed to set address mode: %w", err)
|
return fmt.Errorf("failed to set address mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.publish(events.AddressModeChanged{
|
bridge.publish(events.AddressModeChanged{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
AddressMode: mode,
|
AddressMode: mode,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var splitMode = false
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
if user.GetAddressMode() == vault.SplitMode {
|
||||||
|
splitMode = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bridge.heartbeat.SetSplitMode(splitMode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
// SendBadEventUserFeedback passes the feedback to the given user.
|
||||||
apiUser, err := client.GetUser(ctx)
|
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||||
|
logUser.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||||
|
|
||||||
|
return safe.RLockRet(func() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
user, ok := bridge.users[userID]
|
||||||
|
if !ok {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Failed to handle event: feedback failed: no such user",
|
||||||
|
reporter.Context{"user_id": userID},
|
||||||
|
); rerr != nil {
|
||||||
|
logUser.WithError(rerr).Error("Failed to report feedback failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if doResync {
|
||||||
|
return user.BadEventFeedbackResync(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.logoutUser(ctx, user, true, false)
|
||||||
|
|
||||||
|
bridge.publish(events.UserLoggedOut{
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte, hvDetails *proton.APIHVDetails) (string, error) {
|
||||||
|
apiUser, err := client.GetUserWithHV(ctx, hvDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get API user: %w", err)
|
return "", fmt.Errorf("failed to get API user: %w", err)
|
||||||
}
|
}
|
||||||
@ -315,9 +386,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
||||||
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
||||||
} else if userKR.CountDecryptionEntities() == 0 {
|
} else if userKR.CountDecryptionEntities() == 0 {
|
||||||
return "", fmt.Errorf("failed to unlock user keys")
|
return "", ErrFailedToUnlock
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
||||||
@ -329,30 +400,37 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
|
|
||||||
// loadUsers tries to load each user in the vault that isn't already loaded.
|
// loadUsers tries to load each user in the vault that isn't already loaded.
|
||||||
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
||||||
|
logUser.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users")
|
||||||
|
defer logUser.Info("Finished loading users")
|
||||||
|
|
||||||
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
||||||
|
log := logUser.WithField("userID", user.UserID())
|
||||||
|
|
||||||
if user.AuthUID() == "" {
|
if user.AuthUID() == "" {
|
||||||
|
log.Info("User is not connected (skipping)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
||||||
|
log.Info("User is already loaded (skipping)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithField("userID", user.UserID()).Info("Loading connected user")
|
log.WithField("mode", user.AddressMode()).Info("Loading connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoading{
|
bridge.publish(events.UserLoading{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := bridge.loadUser(ctx, user); err != nil {
|
if err := bridge.loadUser(ctx, user); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to load connected user")
|
log.WithError(err).Error("Failed to load connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoadFail{
|
bridge.publish(events.UserLoadFail{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logrus.WithField("userID", user.UserID()).Info("Successfully loaded user")
|
log.Info("Successfully loaded connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoadSuccess{
|
bridge.publish(events.UserLoadSuccess{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
@ -367,12 +445,13 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
|||||||
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
||||||
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apiErr := new(proton.Error); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
||||||
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
||||||
if err := user.Clear(); err != nil {
|
if err := user.Clear(); err != nil {
|
||||||
logrus.WithError(err).Warn("Failed to clear user secrets")
|
logUser.WithError(err).Warn("Failed to clear user secrets")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("failed to create API client: %w", err)
|
return fmt.Errorf("failed to create API client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,6 +468,12 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
|||||||
return fmt.Errorf("failed to add user: %w", err)
|
return fmt.Errorf("failed to add user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.PrimaryEmail() != apiUser.Email {
|
||||||
|
if err := user.SetPrimaryEmail(apiUser.Email); err != nil {
|
||||||
|
return fmt.Errorf("failed to modify user primary email: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,26 +491,26 @@ func (bridge *Bridge) addUser(
|
|||||||
return fmt.Errorf("failed to add vault user: %w", err)
|
return fmt.Errorf("failed to add vault user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
|
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
|
||||||
if _, ok := err.(*resty.ResponseError); ok || isLogin {
|
if _, ok := err.(*resty.ResponseError); ok || isLogin {
|
||||||
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
|
logUser.WithError(err).Error("Failed to add user, clearing its secrets from vault")
|
||||||
|
|
||||||
if err := vaultUser.Clear(); err != nil {
|
if err := vaultUser.Clear(); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to clear user secrets")
|
logUser.WithError(err).Error("Failed to clear user secrets")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logrus.WithError(err).Error("Failed to add user")
|
logUser.WithError(err).Error("Failed to add user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := vaultUser.Close(); err != nil {
|
if err := vaultUser.Close(); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to close vault user")
|
logUser.WithError(err).Error("Failed to close vault user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isNew {
|
if isNew {
|
||||||
logrus.Warn("Deleting newly added vault user")
|
logUser.Warn("Deleting newly added vault user")
|
||||||
|
|
||||||
if err := bridge.vault.DeleteUser(apiUser.ID); err != nil {
|
if err := bridge.vault.DeleteUser(apiUser.ID); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to delete vault user")
|
logUser.WithError(err).Error("Failed to delete vault user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,40 +526,48 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
client *proton.Client,
|
client *proton.Client,
|
||||||
apiUser proton.User,
|
apiUser proton.User,
|
||||||
vault *vault.User,
|
vault *vault.User,
|
||||||
|
isNew bool,
|
||||||
) error {
|
) error {
|
||||||
|
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
user, err := user.New(
|
user, err := user.New(
|
||||||
ctx,
|
ctx,
|
||||||
vault,
|
vault,
|
||||||
client,
|
client,
|
||||||
bridge.reporter,
|
bridge.reporter,
|
||||||
apiUser,
|
apiUser,
|
||||||
bridge.crashHandler,
|
bridge.panicHandler,
|
||||||
bridge.vault.SyncWorkers(),
|
|
||||||
bridge.vault.GetShowAllMail(),
|
bridge.vault.GetShowAllMail(),
|
||||||
|
bridge.vault.GetMaxSyncMemory(),
|
||||||
|
bridge,
|
||||||
|
bridge.serverManager,
|
||||||
|
bridge.serverManager,
|
||||||
|
&bridgeEventSubscription{b: bridge},
|
||||||
|
bridge.syncService,
|
||||||
|
bridge.observabilityService,
|
||||||
|
syncSettingsPath,
|
||||||
|
isNew,
|
||||||
|
bridge.notificationStore,
|
||||||
|
bridge.unleashService.GetFlagValue,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect the user's address(es) to gluon.
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle events coming from the user before forwarding them to the bridge.
|
// Handle events coming from the user before forwarding them to the bridge.
|
||||||
// For example, if the user's addresses change, we need to update them in gluon.
|
// For example, if the user's addresses change, we need to update them in gluon.
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) {
|
async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) {
|
||||||
logrus.WithFields(logrus.Fields{
|
logUser.WithFields(logrus.Fields{
|
||||||
"userID": apiUser.ID,
|
"userID": apiUser.ID,
|
||||||
"event": event,
|
"event": event,
|
||||||
}).Debug("Received user event")
|
}).Debug("Received user event")
|
||||||
|
|
||||||
if err := bridge.handleUserEvent(ctx, user, event); err != nil {
|
bridge.handleUserEvent(ctx, user, event)
|
||||||
logrus.WithError(err).Error("Failed to handle user event")
|
bridge.publish(event)
|
||||||
} else {
|
|
||||||
bridge.publish(event)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -482,7 +575,7 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
// As such, if we find this ID in the context, we should use it to update our user agent.
|
// As such, if we find this ID in the context, we should use it to update our user agent.
|
||||||
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
|
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
|
||||||
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
|
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
|
||||||
bridge.identifier.SetClient(imapID.Name, imapID.Version)
|
bridge.setUserAgent(imapID.Name, imapID.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -491,8 +584,17 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
// Finally, save the user in the bridge.
|
// Finally, save the user in the bridge.
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
bridge.users[apiUser.ID] = user
|
bridge.users[apiUser.ID] = user
|
||||||
|
bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users))
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
|
// Set user plan if its of a higher rank.
|
||||||
|
bridge.heartbeat.SetUserPlan(user.GetUserPlanName())
|
||||||
|
|
||||||
|
// As we need at least one user to send heartbeat, try to send it.
|
||||||
|
bridge.heartbeat.start()
|
||||||
|
|
||||||
|
user.PublishEvent(ctx, events.UserLoadedCheckResync{UserID: user.ID()})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,58 +605,40 @@ func (bridge *Bridge) newVaultUser(
|
|||||||
authUID, authRef string,
|
authUID, authRef string,
|
||||||
saltedKeyPass []byte,
|
saltedKeyPass []byte,
|
||||||
) (*vault.User, bool, error) {
|
) (*vault.User, bool, error) {
|
||||||
if !bridge.vault.HasUser(apiUser.ID) {
|
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
||||||
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, authUID, authRef, saltedKeyPass)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := bridge.vault.NewUser(apiUser.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetAuth(authUID, authRef); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetKeyPass(saltedKeyPass); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// logout logs out the given user, optionally logging them out from the API too.
|
// logoutUser logs out the given user, optionally logging them out from the API and deleting user related gluon data.
|
||||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
|
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
|
||||||
defer delete(bridge.users, user.ID())
|
defer delete(bridge.users, user.ID())
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logUser.WithFields(logrus.Fields{
|
||||||
"userID": user.ID(),
|
"userID": user.ID(),
|
||||||
"withAPI": withAPI,
|
"withAPI": withAPI,
|
||||||
"withData": withData,
|
"withData": withData,
|
||||||
}).Debug("Logging out user")
|
}).Debug("Logging out user")
|
||||||
|
|
||||||
if err := bridge.removeIMAPUser(ctx, user, withData); err != nil {
|
if err := user.Logout(ctx, withAPI, withData, bridge.unleashService.GetFlagValue(unleash.UserRemovalGluonDataCleanupDisabled)); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
logUser.WithError(err).Error("Failed to logout user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.Logout(ctx, withAPI); err != nil {
|
bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users) - 1)
|
||||||
logrus.WithError(err).Error("Failed to logout user")
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Close()
|
user.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUserInfo returns information about a disconnected user.
|
// getUserInfo returns information about a disconnected user.
|
||||||
func getUserInfo(userID, username string, state UserState, addressMode vault.AddressMode) UserInfo {
|
func getUserInfo(userID, username, primaryEmail string, state UserState, addressMode vault.AddressMode) UserInfo {
|
||||||
|
var addresses []string
|
||||||
|
if len(primaryEmail) > 0 {
|
||||||
|
addresses = []string{primaryEmail}
|
||||||
|
}
|
||||||
|
|
||||||
return UserInfo{
|
return UserInfo{
|
||||||
State: state,
|
State: state,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Addresses: addresses,
|
||||||
AddressMode: addressMode,
|
AddressMode: addressMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
920
internal/bridge/user_event_test.go
Normal file
920
internal/bridge/user_event_test.go
Normal file
@ -0,0 +1,920 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
|
"github.com/bradenaw/juniper/stream"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBridge_User_RefreshEvent(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove a message
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0]))
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, s.RefreshUser(userID, proton.RefreshMail))
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
closeCh()
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_BadMessage_BadEvent(t *testing.T) {
|
||||||
|
t.Run("Resync", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
|
||||||
|
// User feedback is resync
|
||||||
|
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, true))
|
||||||
|
|
||||||
|
// Wait for sync to finish
|
||||||
|
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
require.Equal(t, badUserID, (<-syncCh).UserID)
|
||||||
|
closeCh()
|
||||||
|
}))
|
||||||
|
|
||||||
|
t.Run("LogoutAndLogin", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
|
||||||
|
logoutCh, closeCh := chToType[events.Event, events.UserLoggedOut](bridge.GetEvents(events.UserLoggedOut{}))
|
||||||
|
|
||||||
|
// User feedback is logout
|
||||||
|
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, false))
|
||||||
|
|
||||||
|
require.Equal(t, badUserID, (<-logoutCh).UserID)
|
||||||
|
closeCh()
|
||||||
|
|
||||||
|
// The user will eventually be logged out due to the bad request errors.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
|
||||||
|
// Login again
|
||||||
|
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string)) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||||
|
doBadRequest := true
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
if !doBadRequest {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
|
||||||
|
return "/mail/v4/messages/" + messageID
|
||||||
|
}), req.URL.Path) < 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusBadRequest, true
|
||||||
|
})
|
||||||
|
|
||||||
|
badUserID := userReceivesBadError(t, bridge, mocks)
|
||||||
|
|
||||||
|
// Remove messages, make response OK again
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0:5]...))
|
||||||
|
})
|
||||||
|
doBadRequest = false
|
||||||
|
|
||||||
|
userFeedback(t, ctx, bridge, badUserID)
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
_, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
if strings.Contains(req.URL.Path, "/mail/v4/messages/"+messageIDs[2]) {
|
||||||
|
return http.StatusUnprocessableEntity, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove messages
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||||
|
})
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_SameMessageLabelCreated_NoBadEvent(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Add NOOP events
|
||||||
|
require.NoError(t, s.AddLabelCreatedEvent(userID, labelID))
|
||||||
|
require.NoError(t, s.AddMessageCreatedEvent(userID, messageIDs[9]))
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_MessageLabelDeleted_NoBadEvent(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
// Create and delete 10 more messages for the user, generating delete events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs := createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create and delete 10 labels for the user, generating delete events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
label, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||||
|
Name: uuid.NewString(),
|
||||||
|
Color: "#f66",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, c.DeleteLabel(ctx, label.ID))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
addrID, err = s.CreateAddress(userID, "other@pm.me", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
require.NoError(t, s.AddAddressCreatedEvent(userID, addrID))
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
|
||||||
|
otherID, err := s.CreateAddress(userID, "another@pm.me", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, s.RemoveAddress(userID, otherID))
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
require.NoError(t, s.CreateAddressKey(userID, addrID, password))
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
require.NoError(t, s.RemoveAddress(userID, addrID))
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, _, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
_, err := s.CreateAddressAsUpdate(userID, "another@pm.me", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
retVal := int32(0)
|
||||||
|
|
||||||
|
setResponseAndWait := func(status int32) {
|
||||||
|
atomic.StoreInt32(&retVal, status)
|
||||||
|
time.Sleep(user.EventPeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
status := atomic.LoadInt32(&retVal)
|
||||||
|
if strings.Contains(req.URL.Path, "/core/v4/events/") {
|
||||||
|
return int(status), status != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a user.
|
||||||
|
_, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
|
||||||
|
setResponseAndWait(http.StatusInternalServerError)
|
||||||
|
setResponseAndWait(http.StatusServiceUnavailable)
|
||||||
|
setResponseAndWait(http.StatusPaymentRequired)
|
||||||
|
setResponseAndWait(http.StatusForbidden)
|
||||||
|
setResponseAndWait(http.StatusBadRequest)
|
||||||
|
setResponseAndWait(http.StatusUnprocessableEntity)
|
||||||
|
setResponseAndWait(http.StatusTooManyRequests)
|
||||||
|
time.Sleep(10 * time.Second) // needs minimum of 10 seconds to retry
|
||||||
|
})
|
||||||
|
|
||||||
|
setResponseAndWait(0)
|
||||||
|
time.Sleep(10 * time.Second) // needs up to 20 seconds to retry
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||||
|
defer func() { _ = dropListener.Close() }()
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
_, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
var count int32
|
||||||
|
// The first 10 times bridge attempts to sync any of the messages, drop the connection.
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
if strings.Contains(req.URL.Path, "/mail/v4/messages") {
|
||||||
|
if atomic.AddInt32(&count, 1) < 10 {
|
||||||
|
dropListener.DropAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = cli.Logout() }()
|
||||||
|
|
||||||
|
// The IMAP client will eventually see 20 messages.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := cli.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||||
|
return err == nil && status.Messages == 20
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
}, server.WithListener(dropListener))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_UpdateDraft(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a bridge user.
|
||||||
|
_, _, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Initially sync the user.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
})
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
user, err := c.GetUser(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
addrs, err := c.GetAddresses(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
salts, err := c.GetSalts(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a draft (generating a "create draft message" event).
|
||||||
|
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||||
|
Message: proton.DraftTemplate{
|
||||||
|
Subject: "subject",
|
||||||
|
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||||
|
Body: "body",
|
||||||
|
MIMEType: rfc822.TextPlain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, draft.ReplyTos)
|
||||||
|
|
||||||
|
// Process those events
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the draft (generating an "update draft message" event).
|
||||||
|
draft2, err := c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
|
||||||
|
Message: proton.DraftTemplate{
|
||||||
|
Subject: "subject 2",
|
||||||
|
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||||
|
Body: "body 2",
|
||||||
|
MIMEType: rfc822.TextPlain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, draft2.ReplyTos)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a bridge user.
|
||||||
|
_, _, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Initially sync the user.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
})
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
user, err := c.GetUser(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
addrs, err := c.GetAddresses(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
salts, err := c.GetSalts(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a draft (generating a "create draft message" event).
|
||||||
|
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||||
|
Message: proton.DraftTemplate{
|
||||||
|
Subject: "subject",
|
||||||
|
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||||
|
Body: "body",
|
||||||
|
MIMEType: rfc822.TextPlain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Process those events
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the draft (generating an "update draft message" event).
|
||||||
|
require.NoError(t, getErr(c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
|
||||||
|
Message: proton.DraftTemplate{
|
||||||
|
Subject: "subject 2",
|
||||||
|
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||||
|
Body: "body 2",
|
||||||
|
MIMEType: rfc822.TextPlain,
|
||||||
|
},
|
||||||
|
})))
|
||||||
|
|
||||||
|
// Import a message (generating a "create message" event).
|
||||||
|
str, err := c.ImportMessages(ctx, addrKRs[addrs[0].ID], 1, 1, proton.ImportReq{
|
||||||
|
Metadata: proton.ImportMetadata{
|
||||||
|
AddressID: addrs[0].ID,
|
||||||
|
Flags: proton.MessageFlagReceived,
|
||||||
|
},
|
||||||
|
Message: []byte("From: someone@example.com\r\nTo: blabla@example.com\r\n\r\nhello"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := stream.Collect(ctx, str)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Process those events.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the imported message (generating an "update message" event).
|
||||||
|
require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID))
|
||||||
|
|
||||||
|
// Process those events.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a bridge user.
|
||||||
|
_, _, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Initially sync the user.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
})
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
user, err := c.GetUser(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
addrs, err := c.GetAddresses(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
salts, err := c.GetSalts(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a draft (generating a "create draft message" event).
|
||||||
|
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||||
|
Message: proton.DraftTemplate{
|
||||||
|
Subject: "subject",
|
||||||
|
ToList: []*mail.Address{{Address: addrs[0].Email}},
|
||||||
|
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||||
|
Body: "body",
|
||||||
|
MIMEType: rfc822.TextPlain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Process those events
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = cli.Logout() }()
|
||||||
|
|
||||||
|
messages, err := clientFetch(cli, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send the draft (generating an "update message" event).
|
||||||
|
{
|
||||||
|
pubKeys, recType, err := c.GetPublicKeys(ctx, addrs[0].Email)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, recType, proton.RecipientTypeInternal)
|
||||||
|
|
||||||
|
var req proton.SendDraftReq
|
||||||
|
|
||||||
|
require.NoError(t, req.AddTextPackage(addrKRs[addrs[0].ID], "body", rfc822.TextPlain, map[string]proton.SendPreferences{
|
||||||
|
addrs[0].Email: {
|
||||||
|
Encrypt: true,
|
||||||
|
PubKey: must(crypto.NewKeyRing(must(crypto.NewKeyFromArmored(pubKeys[0].PublicKey)))),
|
||||||
|
SignatureType: proton.DetachedSignature,
|
||||||
|
EncryptionScheme: proton.InternalScheme,
|
||||||
|
MIMEType: rfc822.TextPlain,
|
||||||
|
},
|
||||||
|
}, nil))
|
||||||
|
|
||||||
|
require.NoError(t, getErr(c.SendDraft(ctx, draft.ID, req)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process those events; the draft will move to the sent folder and lose the draft flag.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = cli.Logout() }()
|
||||||
|
|
||||||
|
messages, err := clientFetch(cli, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, _, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create an additional address for the user.
|
||||||
|
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||||
|
|
||||||
|
// Initially we should list the address.
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable the address.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Eventually we shouldn't list the address.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) < 0
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Enable the address.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.EnableAddress(ctx, aliasID))
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Eventually we should list the address.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) >= 0
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, _, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create an additional address for the user.
|
||||||
|
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Immediately disable the address.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||||
|
|
||||||
|
// Initially we shouldn't list the address.
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotContains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
|
||||||
|
info, err := bridge.QueryUserInfo(username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = cli.Logout() }()
|
||||||
|
|
||||||
|
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
parentName := uuid.NewString()
|
||||||
|
childName := uuid.NewString()
|
||||||
|
|
||||||
|
// Create a folder.
|
||||||
|
parentLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||||
|
Name: parentName,
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
Color: "#f66",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for the parent folder to be created.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||||
|
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
|
||||||
|
}) >= 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
|
||||||
|
// Create a subfolder.
|
||||||
|
childLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||||
|
Name: childName,
|
||||||
|
Type: proton.LabelTypeFolder,
|
||||||
|
Color: "#f66",
|
||||||
|
ParentID: parentLabel.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, parentLabel.ID, childLabel.ParentID)
|
||||||
|
|
||||||
|
// Wait for the parent folder to be created.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||||
|
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
|
||||||
|
}) >= 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
|
||||||
|
newParentName := uuid.NewString()
|
||||||
|
|
||||||
|
// Rename the parent folder.
|
||||||
|
require.NoError(t, getErr(c.UpdateLabel(ctx, parentLabel.ID, proton.UpdateLabelReq{
|
||||||
|
Color: "#f66",
|
||||||
|
Name: newParentName,
|
||||||
|
})))
|
||||||
|
|
||||||
|
// Wait for the parent folder to be renamed.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||||
|
return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
|
||||||
|
}) >= 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
|
||||||
|
// Wait for the child folder to be renamed.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||||
|
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
|
||||||
|
}) >= 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// userLoginAndSync logs in user and waits until user is fully synced.
|
||||||
|
func userLoginAndSync(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
bridge *bridge.Bridge,
|
||||||
|
username string, password []byte, //nolint:unparam
|
||||||
|
) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userReceivesBadError(
|
||||||
|
t *testing.T,
|
||||||
|
bridge *bridge.Bridge,
|
||||||
|
mocks *bridge.Mocks,
|
||||||
|
) (userID string) {
|
||||||
|
badEventCh, closeCh := bridge.GetEvents(events.UserBadEvent{})
|
||||||
|
|
||||||
|
// The user will continue to process events and will receive bad request errors.
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||||
|
|
||||||
|
badEvent, ok := (<-badEventCh).(events.UserBadEvent)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
closeCh()
|
||||||
|
|
||||||
|
return badEvent.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
func userContinueEventProcess(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
s *server.Server,
|
||||||
|
bridge *bridge.Bridge,
|
||||||
|
) {
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = cli.Logout() }()
|
||||||
|
|
||||||
|
randomLabel := uuid.NewString()
|
||||||
|
|
||||||
|
// Create a new label.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, getErr(c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||||
|
Name: randomLabel,
|
||||||
|
Color: "#f66",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the label to be created.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||||
|
return mailbox.Name == "Labels/"+randomLabel
|
||||||
|
}) >= 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventuallyDial(addr string) (cli *client.Client, err error) {
|
||||||
|
var sleep = 1 * time.Second
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
cli, err := client.Dial(addr)
|
||||||
|
if err == nil {
|
||||||
|
return cli, nil
|
||||||
|
}
|
||||||
|
time.Sleep(sleep)
|
||||||
|
sleep *= 2
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("after 5 attempts, last error: %s", err)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -19,110 +19,26 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) {
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case events.UserAddressCreated:
|
|
||||||
if err := bridge.handleUserAddressCreated(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user address created event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserAddressUpdated:
|
|
||||||
if err := bridge.handleUserAddressUpdated(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user address updated event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserAddressDeleted:
|
|
||||||
if err := bridge.handleUserAddressDeleted(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user address deleted event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserRefreshed:
|
|
||||||
if err := bridge.handleUserRefreshed(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user refreshed event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserDeauth:
|
case events.UserDeauth:
|
||||||
bridge.handleUserDeauth(ctx, user)
|
bridge.handleUserDeauth(ctx, user)
|
||||||
|
|
||||||
|
case events.UserBadEvent:
|
||||||
|
bridge.handleUserBadEvent(ctx, user, event)
|
||||||
|
|
||||||
|
case events.UserLoadedCheckResync:
|
||||||
|
user.VerifyResyncAndExecute()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
|
||||||
if user.GetAddressMode() == vault.SplitMode {
|
|
||||||
if bridge.imapServer == nil {
|
|
||||||
return fmt.Errorf("no imap server instance running")
|
|
||||||
}
|
|
||||||
|
|
||||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GODT-1948: Handle addresses that have been disabled!
|
|
||||||
func (bridge *Bridge) handleUserAddressUpdated(_ context.Context, user *user.User, _ events.UserAddressUpdated) error {
|
|
||||||
switch user.GetAddressMode() {
|
|
||||||
case vault.CombinedMode:
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
|
|
||||||
case vault.SplitMode:
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
|
|
||||||
if user.GetAddressMode() == vault.SplitMode {
|
|
||||||
if bridge.imapServer == nil {
|
|
||||||
return fmt.Errorf("no imap server instance running")
|
|
||||||
}
|
|
||||||
|
|
||||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User) error {
|
|
||||||
return safe.RLockRet(func() error {
|
|
||||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, bridge.usersLock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||||
@ -130,3 +46,20 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
|||||||
bridge.logoutUser(ctx, user, false, false)
|
bridge.logoutUser(ctx, user, false, false)
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
||||||
|
safe.RLock(func() {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||||
|
"user_id": user.ID(),
|
||||||
|
"old_event_id": event.OldEventID,
|
||||||
|
"new_event_id": event.NewEventID,
|
||||||
|
"event_info": event.EventInfo,
|
||||||
|
"error": event.Error,
|
||||||
|
"error_type": internal.ErrCauseType(event.Error),
|
||||||
|
}); rerr != nil {
|
||||||
|
logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
|
||||||
|
}
|
||||||
|
|
||||||
|
user.OnBadEvent(ctx)
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -20,27 +20,27 @@ package bridge_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
mocksPkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBridge_WithoutUsers(t *testing.T) {
|
func TestBridge_WithoutUsers(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Empty(t, bridge.GetUserIDs())
|
require.Empty(t, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Empty(t, bridge.GetUserIDs())
|
require.Empty(t, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
})
|
})
|
||||||
@ -49,7 +49,7 @@ func TestBridge_WithoutUsers(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_Login(t *testing.T) {
|
func TestBridge_Login(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -61,9 +61,53 @@ func TestBridge_Login(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_Login_DropConn(t *testing.T) {
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||||
|
defer func() { _ = dropListener.Close() }()
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Login the user.
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The user is now connected.
|
||||||
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
|
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Whether to allow the user to be created.
|
||||||
|
var allowUser bool
|
||||||
|
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
// Drop any request to the users endpoint.
|
||||||
|
if !allowUser && req.URL.Path == "/core/v4/users" {
|
||||||
|
dropListener.DropAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the ping request, allow the user to be created.
|
||||||
|
if req.URL.Path == "/tests/ping" {
|
||||||
|
allowUser = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// The user is eventually connected.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
}, server.WithListener(dropListener))
|
||||||
|
}
|
||||||
|
|
||||||
func TestBridge_LoginTwice(t *testing.T) {
|
func TestBridge_LoginTwice(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -81,7 +125,7 @@ func TestBridge_LoginTwice(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_LoginLogoutLogin(t *testing.T) {
|
func TestBridge_LoginLogoutLogin(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -109,7 +153,7 @@ func TestBridge_LoginLogoutLogin(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_LoginDeleteLogin(t *testing.T) {
|
func TestBridge_LoginDeleteLogin(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -137,7 +181,7 @@ func TestBridge_LoginDeleteLogin(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_LoginDeauthLogin(t *testing.T) {
|
func TestBridge_LoginDeauthLogin(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -171,7 +215,7 @@ func TestBridge_LoginDeauthRestartLogin(t *testing.T) {
|
|||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -191,7 +235,7 @@ func TestBridge_LoginDeauthRestartLogin(t *testing.T) {
|
|||||||
require.IsType(t, events.UserDeauth{}, <-eventCh)
|
require.IsType(t, events.UserDeauth{}, <-eventCh)
|
||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// The user should be disconnected at startup.
|
// The user should be disconnected at startup.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
@ -213,7 +257,7 @@ func TestBridge_LoginExpireLogin(t *testing.T) {
|
|||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
s.SetAuthLife(authLife)
|
s.SetAuthLife(authLife)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user. Its auth will only be valid for a short time.
|
// Login the user. Its auth will only be valid for a short time.
|
||||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -231,7 +275,7 @@ func TestBridge_FailToLoad(t *testing.T) {
|
|||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
// Login the user.
|
// Login the user.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -239,7 +283,7 @@ func TestBridge_FailToLoad(t *testing.T) {
|
|||||||
require.NoError(t, s.RevokeUser(userID))
|
require.NoError(t, s.RevokeUser(userID))
|
||||||
|
|
||||||
// When bridge starts, the user will not be logged in.
|
// When bridge starts, the user will not be logged in.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
})
|
})
|
||||||
@ -251,7 +295,7 @@ func TestBridge_LoadWithoutInternet(t *testing.T) {
|
|||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
// Login the user.
|
// Login the user.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -259,7 +303,7 @@ func TestBridge_LoadWithoutInternet(t *testing.T) {
|
|||||||
netCtl.Disable()
|
netCtl.Disable()
|
||||||
|
|
||||||
// Start bridge without internet.
|
// Start bridge without internet.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Initially, users are not connected.
|
// Initially, users are not connected.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
@ -281,11 +325,11 @@ func TestBridge_LoginRestart(t *testing.T) {
|
|||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||||
})
|
})
|
||||||
@ -296,7 +340,7 @@ func TestBridge_LoginLogoutRestart(t *testing.T) {
|
|||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -304,7 +348,7 @@ func TestBridge_LoginLogoutRestart(t *testing.T) {
|
|||||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// The user is still disconnected.
|
// The user is still disconnected.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
@ -316,7 +360,7 @@ func TestBridge_LoginDeleteRestart(t *testing.T) {
|
|||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -324,7 +368,7 @@ func TestBridge_LoginDeleteRestart(t *testing.T) {
|
|||||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// The user is still gone.
|
// The user is still gone.
|
||||||
require.Empty(t, bridge.GetUserIDs())
|
require.Empty(t, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
@ -340,7 +384,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
|||||||
|
|
||||||
// Log the user in, wait for it to sync, then log it out.
|
// Log the user in, wait for it to sync, then log it out.
|
||||||
// (We don't want to count message sync data in the test.)
|
// (We don't want to count message sync data in the test.)
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -352,7 +396,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
|||||||
var total uint64
|
var total uint64
|
||||||
|
|
||||||
// Now that the user is synced, we can measure exactly how much data is needed during login.
|
// Now that the user is synced, we can measure exactly how much data is needed during login.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
total = countBytesRead(netCtl, func() {
|
total = countBytesRead(netCtl, func() {
|
||||||
must(bridge.LoginFull(ctx, username, password, nil, nil))
|
must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
})
|
})
|
||||||
@ -361,7 +405,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Now simulate failing to login.
|
// Now simulate failing to login.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Simulate a partial read.
|
// Simulate a partial read.
|
||||||
netCtl.SetReadLimit(i * total / 10)
|
netCtl.SetReadLimit(i * total / 10)
|
||||||
|
|
||||||
@ -377,7 +421,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
|||||||
netCtl.SetReadLimit(0)
|
netCtl.SetReadLimit(0)
|
||||||
|
|
||||||
// We should now be able to log the user in.
|
// We should now be able to log the user in.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
|
||||||
// The user should be there, now connected.
|
// The user should be there, now connected.
|
||||||
@ -397,7 +441,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
|||||||
|
|
||||||
// Log the user in and wait for it to sync.
|
// Log the user in and wait for it to sync.
|
||||||
// (We don't want to count message sync data in the test.)
|
// (We don't want to count message sync data in the test.)
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -407,7 +451,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
|||||||
|
|
||||||
// See how much data it takes to load the user at startup.
|
// See how much data it takes to load the user at startup.
|
||||||
total := countBytesRead(netCtl, func() {
|
total := countBytesRead(netCtl, func() {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -416,7 +460,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
|||||||
netCtl.SetReadLimit(i * total / 10)
|
netCtl.SetReadLimit(i * total / 10)
|
||||||
|
|
||||||
// We should fail to load the user; it should be listed but disconnected.
|
// We should fail to load the user; it should be listed but disconnected.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
})
|
})
|
||||||
@ -425,7 +469,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
|||||||
netCtl.SetReadLimit(0)
|
netCtl.SetReadLimit(0)
|
||||||
|
|
||||||
// We should now be able to load the user.
|
// We should now be able to load the user.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||||
})
|
})
|
||||||
@ -440,7 +484,7 @@ func TestBridge_BridgePass(t *testing.T) {
|
|||||||
|
|
||||||
var pass []byte
|
var pass []byte
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -457,7 +501,7 @@ func TestBridge_BridgePass(t *testing.T) {
|
|||||||
require.Equal(t, pass, pass)
|
require.Equal(t, pass, pass)
|
||||||
})
|
})
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// The bridge should load the user.
|
// The bridge should load the user.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||||
@ -470,7 +514,7 @@ func TestBridge_BridgePass(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_AddressMode(t *testing.T) {
|
func TestBridge_AddressMode(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -508,7 +552,7 @@ func TestBridge_AddressMode(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_LoginLogoutRepeated(t *testing.T) {
|
func TestBridge_LoginLogoutRepeated(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
// Log the user in.
|
// Log the user in.
|
||||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
@ -524,7 +568,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
|
|||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
var userID string
|
var userID string
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||||
|
|
||||||
@ -546,7 +590,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
|
|||||||
// Go back online.
|
// Go back online.
|
||||||
netCtl.Enable()
|
netCtl.Enable()
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// The user is still disconnected.
|
// The user is still disconnected.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||||
@ -556,7 +600,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_DeleteDisconnected(t *testing.T) {
|
func TestBridge_DeleteDisconnected(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -584,7 +628,7 @@ func TestBridge_DeleteDisconnected(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_DeleteOffline(t *testing.T) {
|
func TestBridge_DeleteOffline(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -608,7 +652,7 @@ func TestBridge_DeleteOffline(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_UserInfo_Alias(t *testing.T) {
|
func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
// Create a new user.
|
// Create a new user.
|
||||||
userID, _, err := s.CreateUser("primary", []byte("password"))
|
userID, _, err := s.CreateUser("primary", []byte("password"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -631,12 +675,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
|||||||
|
|
||||||
func TestBridge_User_Refresh(t *testing.T) {
|
func TestBridge_User_Refresh(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
|
||||||
gomock.Eq("Warning: refresh occurred"),
|
|
||||||
mocksPkg.NewRefreshContextMatcher(proton.RefreshAll),
|
|
||||||
).Return(nil)
|
|
||||||
|
|
||||||
// Get a channel of sync started events.
|
// Get a channel of sync started events.
|
||||||
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
defer done()
|
defer done()
|
||||||
@ -662,7 +701,26 @@ func TestBridge_User_Refresh(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_GetAddresses(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, _, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
info, err := bridge.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(info.Addresses))
|
||||||
|
require.Equal(t, info.Addresses[0], "user@proton.local")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// getErr returns the error that was passed to it.
|
// getErr returns the error that was passed to it.
|
||||||
func getErr[T any](val T, err error) error {
|
func getErr[T any](_ T, err error) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -17,69 +17,411 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
import (
|
/*
|
||||||
"os"
|
#cgo CFLAGS: -x objective-c
|
||||||
|
#cgo LDFLAGS: -framework Foundation -framework Security
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
|
||||||
"golang.org/x/sys/execabs"
|
// Memory management rules:
|
||||||
|
// Foundation object (Objective-C prefixed with `NS`) get ARC (Automatic Reference Counting), and do not need to be released manually.
|
||||||
|
// Core Foundation objects (C), prefixed with need to be released manually using CFRelease() unless:
|
||||||
|
// - They're obtained using a CF method containing the word Get (a.k.a. the Get Rule).
|
||||||
|
// - They're obtained using toll-free bridging from a Foundation Object (using the __bridge keyword).
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Create a certificate object from DER-encoded data.
|
||||||
|
///
|
||||||
|
/// \return The certifcation. The caller is responsible for releasing the object using CFRelease.
|
||||||
|
/// \return NULL if data is not a valid DER-encoded certificate.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
SecCertificateRef certFromData(char const* data, uint64_t length) {
|
||||||
|
NSData *der = [NSData dataWithBytes:data length:length];
|
||||||
|
return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Check if a certificate is in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return true iff the certificate is in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool _isCertificateInKeychain(SecCertificateRef const cert) {
|
||||||
|
NSDictionary *attrs = @{
|
||||||
|
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||||
|
(id)kSecClass: (id)kSecClassCertificate,
|
||||||
|
(id)kSecReturnData: @YES
|
||||||
|
};
|
||||||
|
return errSecSuccess == SecItemCopyMatching((__bridge CFDictionaryRef)attrs, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Check if a certificate is in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return true iff the certificate is in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool isCertificateInKeychain(char const* certData, uint64_t certSize) {
|
||||||
|
return _isCertificateInKeychain(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Add a certificate to the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _addCertificateToKeychain(SecCertificateRef const cert) {
|
||||||
|
NSDictionary* addQuery = @{
|
||||||
|
(id)kSecValueRef: (__bridge id) cert,
|
||||||
|
(id)kSecClass: (id)kSecClassCertificate,
|
||||||
|
};
|
||||||
|
return SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Add a certificate to the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus addCertificateToKeychain(char const* certData, uint64_t certSize) {
|
||||||
|
return _addCertificateToKeychain(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Add a certificate to the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _removeCertificateFromKeychain(SecCertificateRef const cert) {
|
||||||
|
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
||||||
|
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||||
|
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
||||||
|
};
|
||||||
|
return SecItemDelete((__bridge CFDictionaryRef) query);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Add a certificate to the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus removeCertificateFromKeychain(char const* certData, uint64_t certSize) {
|
||||||
|
return _removeCertificateFromKeychain(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Check if a certificate is trusted in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return true iff the certificate is trusted in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool _isCertificateTrusted(SecCertificateRef const cert) {
|
||||||
|
CFArrayRef trustSettings = NULL;
|
||||||
|
OSStatus status = SecTrustSettingsCopyTrustSettings(cert, kSecTrustSettingsDomainUser, &trustSettings);
|
||||||
|
if (status != errSecSuccess) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CFIndex count = CFArrayGetCount(trustSettings);
|
||||||
|
bool result = false;
|
||||||
|
for (CFIndex index = 0; index < count; ++index) {
|
||||||
|
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(trustSettings, index);
|
||||||
|
if (!dict) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CFNumberRef num = (CFNumberRef)CFDictionaryGetValue(dict, kSecTrustSettingsResult);
|
||||||
|
int value;
|
||||||
|
if (num && CFNumberGetValue(num, kCFNumberSInt32Type, &value) && (value == kSecTrustSettingsResultTrustRoot)) {
|
||||||
|
result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CFRelease(trustSettings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Check if a certificate is trusted in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return true iff the certificate is trusted in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool isCertificateTrusted(char const* certData, uint64_t certSize) {
|
||||||
|
return _isCertificateTrusted(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Set the trust level for a certificate in the user's keychain. This call will trigger a security prompt.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \param[in] trustLevel The trust level.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _setCertificateTrustLevel(SecCertificateRef const cert, int trustLevel) {
|
||||||
|
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
||||||
|
NSDictionary *trustSettings = @{
|
||||||
|
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:trustLevel],
|
||||||
|
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||||
|
};
|
||||||
|
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||||
|
CFRelease(policy);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _setCertificateTrusted(SecCertificateRef cert) {
|
||||||
|
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultTrustRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus setCertificateTrusted(char const* certData, uint64_t certSize) {
|
||||||
|
return _setCertificateTrusted(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Remove the trust level of a certificate in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _removeCertificateTrust(SecCertificateRef cert) {
|
||||||
|
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultUnspecified);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Remove the trust level of a certificate in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus removeCertificateTrust(char const* certData, uint64_t certSize) {
|
||||||
|
return _removeCertificateTrust(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// some of the error codes returned by Apple's Security framework.
|
||||||
|
const (
|
||||||
|
errSecSuccess = 0
|
||||||
|
errAuthorizationCanceled = -60006
|
||||||
|
)
|
||||||
|
|
||||||
|
// certPEMToDER converts a certificate in PEM format to DER format, which is the format required by Apple's Security framework.
|
||||||
|
func certPEMToDER(certPEM []byte) ([]byte, error) {
|
||||||
|
block, left := pem.Decode(certPEM)
|
||||||
|
if block == nil {
|
||||||
|
return []byte{}, errors.New("invalid PEM certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(left) > 0 {
|
||||||
|
return []byte{}, errors.New("trailing data found at the end of a PEM certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return block.Bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapCGoCertCallReturningBool wrap call to a CGo function returning a bool.
|
||||||
|
// if the certificate is invalid the call will return false.
|
||||||
|
func wrapCGoCertCallReturningBool(certPEM []byte, fn func(*C.char, C.ulonglong) bool) bool {
|
||||||
|
certDER, err := certPEMToDER(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return false // error are ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := C.CBytes(certDER)
|
||||||
|
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
||||||
|
|
||||||
|
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapCGoCertCallReturningBool wrap call to a CGo function returning an error
|
||||||
|
func wrapCGoCertCallReturningError(certPEM []byte, fn func(*C.char, C.ulonglong) error) error {
|
||||||
|
certDER, err := certPEMToDER(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := C.CBytes(certDER)
|
||||||
|
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
||||||
|
|
||||||
|
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCertInKeychain returns true if the given certificate is stored in the user's keychain.
|
||||||
|
func isCertInKeychain(certPEM []byte) bool {
|
||||||
|
return wrapCGoCertCallReturningBool(certPEM, isCertInKeychainCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCertInKeychainCGo(buffer *C.char, size C.ulonglong) bool {
|
||||||
|
return bool(C.isCertificateInKeychain(buffer, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addCertToKeychain adds a certificate to the user's keychain.
|
||||||
|
// Trying to add a certificate that is already in the keychain will result in an error.
|
||||||
|
func addCertToKeychain(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, addCertToKeychainCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCertToKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
if errCode := C.addCertificateToKeychain(buffer, size); errCode != errSecSuccess {
|
||||||
|
return fmt.Errorf("could not add certificate to keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeCertFromKeychain removes a certificate from the user's keychain.
|
||||||
|
// Trying to remove a certificate that is not in the keychain will result in an error.
|
||||||
|
func removeCertFromKeychain(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, removeCertFromKeychainCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCertFromKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
if errCode := C.removeCertificateFromKeychain(buffer, size); errCode != errSecSuccess {
|
||||||
|
return fmt.Errorf("could not remove certificate from keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCertTrusted check if a certificate is trusted in the user's keychain.
|
||||||
|
func isCertTrusted(certPEM []byte) bool {
|
||||||
|
return wrapCGoCertCallReturningBool(certPEM, isCertTrustedCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCertTrustedCGo(buffer *C.char, size C.ulonglong) bool {
|
||||||
|
return bool(C.isCertificateTrusted(buffer, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCertTrusted sets a certificate as trusted in the user's keychain.
|
||||||
|
// This function will trigger a security prompt from the system.
|
||||||
|
func setCertTrusted(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, setCertTrustedCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCertTrustedCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
errCode := C.setCertificateTrusted(buffer, size)
|
||||||
|
switch errCode {
|
||||||
|
case errSecSuccess:
|
||||||
|
return nil
|
||||||
|
case errAuthorizationCanceled:
|
||||||
|
return ErrUserCanceledCertificateInstall
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeCertTrust remove the trust level of the certificated from the user's keychain.
|
||||||
|
// This function will trigger a security prompt from the system.
|
||||||
|
func removeCertTrust(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, removeCertTrustCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
errCode := C.removeCertificateTrust(buffer, size)
|
||||||
|
switch errCode {
|
||||||
|
case errSecSuccess:
|
||||||
|
return nil
|
||||||
|
case errAuthorizationCanceled:
|
||||||
|
return ErrUserCanceledCertificateInstall
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func osSupportCertInstall() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
|
||||||
|
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
|
||||||
func installCert(certPEM []byte) error {
|
func installCert(certPEM []byte) error {
|
||||||
name, err := writeToTempFile(certPEM)
|
certDER, err := certPEMToDER(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return addTrustedCert(name)
|
p := C.CBytes(certDER)
|
||||||
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
|
buffer := (*C.char)(p)
|
||||||
|
size := C.ulonglong(len(certDER))
|
||||||
|
|
||||||
|
if !isCertInKeychainCGo(buffer, size) {
|
||||||
|
if err := addCertToKeychainCGo(buffer, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isCertTrustedCGo(buffer, size) {
|
||||||
|
return setCertTrustedCGo(buffer, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uninstallCert uninstalls a certificate in the keychain. The certificate trust is removed and the certificated is deleted from the keychain.
|
||||||
|
// This function will trigger a security prompt from the system, unless the certificate is not trusted in the user keychain.
|
||||||
func uninstallCert(certPEM []byte) error {
|
func uninstallCert(certPEM []byte) error {
|
||||||
name, err := writeToTempFile(certPEM)
|
certDER, err := certPEMToDER(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return removeTrustedCert(name)
|
p := C.CBytes(certDER)
|
||||||
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
|
buffer := (*C.char)(p)
|
||||||
|
size := C.ulonglong(len(certDER))
|
||||||
|
|
||||||
|
if isCertTrustedCGo(buffer, size) {
|
||||||
|
if err := removeCertTrustCGo(buffer, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCertInKeychainCGo(buffer, size) {
|
||||||
|
return removeCertFromKeychainCGo(buffer, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTrustedCert(certPath string) error {
|
func isCertInstalled(certPEM []byte) bool {
|
||||||
return execabs.Command( //nolint:gosec
|
certDER, err := certPEMToDER(certPEM)
|
||||||
"/usr/bin/security",
|
|
||||||
"execute-with-privileges",
|
|
||||||
"/usr/bin/security",
|
|
||||||
"add-trusted-cert",
|
|
||||||
"-d",
|
|
||||||
"-r", "trustRoot",
|
|
||||||
"-p", "ssl",
|
|
||||||
"-k", "/Library/Keychains/System.keychain",
|
|
||||||
certPath,
|
|
||||||
).Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeTrustedCert(certPath string) error {
|
|
||||||
return execabs.Command( //nolint:gosec
|
|
||||||
"/usr/bin/security",
|
|
||||||
"execute-with-privileges",
|
|
||||||
"/usr/bin/security",
|
|
||||||
"remove-trusted-cert",
|
|
||||||
"-d",
|
|
||||||
certPath,
|
|
||||||
).Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeToTempFile writes the given data to a temporary file and returns the path.
|
|
||||||
func writeToTempFile(data []byte) (string, error) {
|
|
||||||
f, err := os.CreateTemp("", "tls")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := f.Write(data); err != nil {
|
p := C.CBytes(certDER)
|
||||||
return "", err
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
}
|
buffer := (*C.char)(p)
|
||||||
|
size := C.ulonglong(len(certDER))
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
return isCertInKeychainCGo(buffer, size) && isCertTrustedCGo(buffer, size)
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.Name(), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
98
internal/certs/cert_store_darwin_test.go
Normal file
98
internal/certs/cert_store_darwin_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCertInKeychain(t *testing.T) {
|
||||||
|
// no trust settings change is performed, so this test will not trigger an OS security prompt.
|
||||||
|
certPEM := generatePEMCertificate(t)
|
||||||
|
require.True(t, osSupportCertInstall())
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
require.NoError(t, addCertToKeychain(certPEM))
|
||||||
|
require.True(t, isCertInKeychain(certPEM))
|
||||||
|
require.Error(t, addCertToKeychain(certPEM))
|
||||||
|
require.True(t, isCertInKeychain(certPEM))
|
||||||
|
require.NoError(t, removeCertFromKeychain(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
require.Error(t, removeCertFromKeychain(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test require human interaction (macOS security prompts), and is disabled by default.
|
||||||
|
func _TestCertificateTrust(t *testing.T) { //nolint:unused
|
||||||
|
certPEM := generatePEMCertificate(t)
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.NoError(t, addCertToKeychain(certPEM))
|
||||||
|
require.NoError(t, setCertTrusted(certPEM))
|
||||||
|
require.True(t, isCertTrusted(certPEM))
|
||||||
|
require.NoError(t, removeCertTrust(certPEM))
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.NoError(t, removeCertFromKeychain(certPEM))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test require human interaction (macOS security prompts), and is disabled by default.
|
||||||
|
func _TestInstallAndRemove(t *testing.T) { //nolint:unused
|
||||||
|
certPEM := generatePEMCertificate(t)
|
||||||
|
|
||||||
|
// fresh install
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
require.NoError(t, installCert(certPEM))
|
||||||
|
require.True(t, isCertInKeychain(certPEM))
|
||||||
|
require.True(t, isCertTrusted(certPEM))
|
||||||
|
require.True(t, isCertInstalled(certPEM))
|
||||||
|
require.NoError(t, uninstallCert(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
|
||||||
|
// Install where certificate is already in Keychain, but not trusted.
|
||||||
|
require.NoError(t, addCertToKeychain(certPEM))
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
require.NoError(t, installCert(certPEM))
|
||||||
|
require.True(t, isCertInstalled(certPEM))
|
||||||
|
|
||||||
|
// Install where certificate is already installed
|
||||||
|
require.NoError(t, installCert(certPEM))
|
||||||
|
|
||||||
|
// Remove when certificate is not trusted.
|
||||||
|
require.NoError(t, removeCertTrust(certPEM))
|
||||||
|
require.NoError(t, uninstallCert(certPEM))
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
|
||||||
|
// Remove when certificate has already been removed.
|
||||||
|
require.NoError(t, uninstallCert(certPEM))
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePEMCertificate(t *testing.T) []byte {
|
||||||
|
template, err := NewTLSTemplate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
certPEM, _, err := GenerateCert(template)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return certPEM
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -17,6 +17,10 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
|
func osSupportCertInstall() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func installCert([]byte) error {
|
func installCert([]byte) error {
|
||||||
return nil // Linux doesn't have a root cert store.
|
return nil // Linux doesn't have a root cert store.
|
||||||
}
|
}
|
||||||
@ -24,3 +28,7 @@ func installCert([]byte) error {
|
|||||||
func uninstallCert([]byte) error {
|
func uninstallCert([]byte) error {
|
||||||
return nil // Linux doesn't have a root cert store.
|
return nil // Linux doesn't have a root cert store.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCertInstalled([]byte) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -17,6 +17,10 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
|
func osSupportCertInstall() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func installCert([]byte) error {
|
func installCert([]byte) error {
|
||||||
return nil // NOTE(GODT-986): Install certs to root cert store?
|
return nil // NOTE(GODT-986): Install certs to root cert store?
|
||||||
}
|
}
|
||||||
@ -24,3 +28,7 @@ func installCert([]byte) error {
|
|||||||
func uninstallCert([]byte) error {
|
func uninstallCert([]byte) error {
|
||||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCertInstalled([]byte) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -17,16 +17,66 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
type Installer struct{}
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Installer struct {
|
||||||
|
log *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
func NewInstaller() *Installer {
|
func NewInstaller() *Installer {
|
||||||
return &Installer{}
|
return &Installer{
|
||||||
|
log: logrus.WithField("pkg", "certs"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (installer *Installer) OSSupportCertInstall() bool {
|
||||||
|
return osSupportCertInstall()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (installer *Installer) InstallCert(certPEM []byte) error {
|
func (installer *Installer) InstallCert(certPEM []byte) error {
|
||||||
return installCert(certPEM)
|
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
|
||||||
|
|
||||||
|
if err := installCert(certPEM); err != nil {
|
||||||
|
installer.log.WithError(err).Error("The Bridge TLS certificate could not be installed in the OS keychain")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installer.log.Info("The Bridge TLS certificate was successfully installed in the OS keychain")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
||||||
return uninstallCert(certPEM)
|
installer.log.Info("Uninstalling the Bridge TLS certificate from the OS keychain")
|
||||||
|
|
||||||
|
if err := uninstallCert(certPEM); err != nil {
|
||||||
|
installer.log.WithError(err).Error("The Bridge TLS certificate could not be uninstalled from the OS keychain")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installer.log.Info("The Bridge TLS certificate was successfully uninstalled from the OS keychain")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
|
||||||
|
return isCertInstalled(certPEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogCertInstallStatus reports the current status of the certificate installation in the log.
|
||||||
|
// If certificate installation is not supported on the platform, this function does nothing.
|
||||||
|
func (installer *Installer) LogCertInstallStatus(certPEM []byte) {
|
||||||
|
if installer.OSSupportCertInstall() {
|
||||||
|
if installer.IsCertInstalled(certPEM) {
|
||||||
|
installer.log.Info("The Bridge TLS certificate is installed in the OS keychain")
|
||||||
|
} else {
|
||||||
|
installer.log.Info("The Bridge TLS certificate is not installed in the OS keychain")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
@ -39,10 +40,10 @@ func (c *AppleMail) Configure(
|
|||||||
hostname string,
|
hostname string,
|
||||||
imapPort, smtpPort int,
|
imapPort, smtpPort int,
|
||||||
imapSSL, smtpSSL bool,
|
imapSSL, smtpSSL bool,
|
||||||
username, addresses string,
|
username, displayName, addresses string,
|
||||||
password []byte,
|
password []byte,
|
||||||
) error {
|
) error {
|
||||||
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
|
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, displayName, addresses, password)
|
||||||
|
|
||||||
confPath, err := saveConfigTemporarily(mc)
|
confPath, err := saveConfigTemporarily(mc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -66,26 +67,28 @@ func prepareMobileConfig(
|
|||||||
hostname string,
|
hostname string,
|
||||||
imapPort, smtpPort int,
|
imapPort, smtpPort int,
|
||||||
imapSSL, smtpSSL bool,
|
imapSSL, smtpSSL bool,
|
||||||
username, addresses string,
|
username, displayName, addresses string,
|
||||||
password []byte,
|
password []byte,
|
||||||
) *mobileconfig.Config {
|
) *mobileconfig.Config {
|
||||||
return &mobileconfig.Config{
|
return &mobileconfig.Config{
|
||||||
DisplayName: username,
|
DisplayName: escapeXMLString(username),
|
||||||
EmailAddress: addresses,
|
EmailAddress: escapeXMLString(addresses),
|
||||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
AccountName: escapeXMLString(displayName),
|
||||||
|
AccountDescription: escapeXMLString(username),
|
||||||
|
Identifier: escapeXMLString("protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10)),
|
||||||
IMAP: &mobileconfig.IMAP{
|
IMAP: &mobileconfig.IMAP{
|
||||||
Hostname: hostname,
|
Hostname: escapeXMLString(hostname),
|
||||||
Port: imapPort,
|
Port: imapPort,
|
||||||
TLS: imapSSL,
|
TLS: imapSSL,
|
||||||
Username: username,
|
Username: escapeXMLString(username),
|
||||||
Password: string(password),
|
Password: escapeXMLString(string(password)),
|
||||||
},
|
},
|
||||||
SMTP: &mobileconfig.SMTP{
|
SMTP: &mobileconfig.SMTP{
|
||||||
Hostname: hostname,
|
Hostname: escapeXMLString(hostname),
|
||||||
Port: smtpPort,
|
Port: smtpPort,
|
||||||
TLS: smtpSSL,
|
TLS: smtpSSL,
|
||||||
Username: username,
|
Username: escapeXMLString(username),
|
||||||
Password: string(password),
|
Password: escapeXMLString(string(password)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,6 +101,8 @@ func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
|
|||||||
|
|
||||||
// Make sure the temporary file is deleted.
|
// Make sure the temporary file is deleted.
|
||||||
go func() {
|
go func() {
|
||||||
|
defer recover() //nolint:errcheck
|
||||||
|
|
||||||
<-time.After(10 * time.Minute)
|
<-time.After(10 * time.Minute)
|
||||||
_ = os.RemoveAll(dir)
|
_ = os.RemoveAll(dir)
|
||||||
}()
|
}()
|
||||||
@ -117,3 +122,13 @@ func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// escapeXMLString replace all occurrences of the 5 characters `&`, `<`, `>`, `"` and `'` by their respective escaped version as per the XML spec.
|
||||||
|
// https://www.w3.org/TR/xml/#syntax
|
||||||
|
func escapeXMLString(input string) string {
|
||||||
|
result := strings.ReplaceAll(input, `&`, `&`)
|
||||||
|
result = strings.ReplaceAll(result, `<`, `<`)
|
||||||
|
result = strings.ReplaceAll(result, `>`, `>`)
|
||||||
|
result = strings.ReplaceAll(result, `"`, `"`)
|
||||||
|
return strings.ReplaceAll(result, `'`, `'`)
|
||||||
|
}
|
||||||
|
|||||||
38
internal/clientconfig/applemail_test.go
Normal file
38
internal/clientconfig/applemail_test.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (c) 2025 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package clientconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEscapeXMLString(t *testing.T) {
|
||||||
|
require.Equal(t, escapeXMLString(`abc&&''""<<>>def`), `abc&&''""<<>>def`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test requires human interaction (user configuration profile installation prompt). It is for debugging purpose and is disabled by default.
|
||||||
|
func _TestInstallCert(t *testing.T) { //nolint:unused
|
||||||
|
require.NoError(
|
||||||
|
t,
|
||||||
|
(&AppleMail{}).Configure(`127.0.0.1`, 1143, 1025, true, false, `user&>>`, `<<abc&&'"def>>`, `user&a`, []byte(`ir8R9vhdNXyB7isWzhyEkQ`)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.Bridge.
|
// This file is part of Proton Mail Bridge.Bridge.
|
||||||
//
|
//
|
||||||
@ -33,9 +33,12 @@ var (
|
|||||||
// Version of the build.
|
// Version of the build.
|
||||||
Version = "0.0.0"
|
Version = "0.0.0"
|
||||||
|
|
||||||
// Revision is current hash of the build.
|
// Revision is build time commit hash.
|
||||||
Revision = ""
|
Revision = ""
|
||||||
|
|
||||||
|
// Tag is build time git describe.
|
||||||
|
Tag = ""
|
||||||
|
|
||||||
// BuildTime stamp of the build.
|
// BuildTime stamp of the build.
|
||||||
BuildTime = ""
|
BuildTime = ""
|
||||||
|
|
||||||
@ -44,6 +47,9 @@ var (
|
|||||||
|
|
||||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||||
DSNSentry = ""
|
DSNSentry = ""
|
||||||
|
|
||||||
|
// BuildEnv tags used at build time.
|
||||||
|
BuildEnv = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2025 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user