Compare commits
2625 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c199f697c | ||
|
|
5c01c3793c | ||
|
|
0ff901ef32 | ||
|
|
08a5d5f6ff | ||
|
|
1fd00def95 | ||
|
|
21a73fa5e6 | ||
|
|
c9b2c3cc36 | ||
|
|
4ee876c134 | ||
|
|
c1c713ef15 | ||
|
|
b926cd5cf2 | ||
|
|
325343c2cf | ||
|
|
d9502ff348 | ||
|
|
585f8f49a7 | ||
|
|
505db999ad | ||
|
|
d402dd5164 | ||
|
|
a93cc2a08f | ||
|
|
0ac8685c63 | ||
|
|
754b2fc40c | ||
|
|
c32a68ed93 | ||
|
|
c269311ef5 | ||
|
|
04c28a9637 | ||
|
|
6830a78e23 | ||
|
|
3a4921344b | ||
|
|
063b77f3df | ||
|
|
61e5f1fde5 | ||
|
|
4f253bfab1 | ||
|
|
c88ca44ff1 | ||
|
|
2d871f2759 | ||
|
|
2a0bac500c | ||
|
|
cedf4de2ba | ||
|
|
d9e1a7ae98 | ||
|
|
80118d579d | ||
|
|
0299c8be99 | ||
|
|
70a7ec54ea | ||
|
|
4876d37287 | ||
|
|
9894ca78f0 | ||
|
|
99a4b4b4e1 | ||
|
|
1169f5bae9 | ||
|
|
a59619c153 | ||
|
|
c4932c4c8e | ||
|
|
cb31699a6e | ||
|
|
41e6e10241 | ||
|
|
ce5c85f592 | ||
|
|
2614514bc6 | ||
|
|
a50c261f13 | ||
|
|
b474193f97 | ||
|
|
27a4f2e8ff | ||
|
|
17024f814a | ||
|
|
d18cc23149 | ||
|
|
047111ffbb | ||
|
|
8c5dbb4197 | ||
|
|
7a6cbab8af | ||
|
|
add6f179a4 | ||
|
|
29d8c41395 | ||
|
|
0ecfa248ee | ||
|
|
748c7c5de1 | ||
|
|
2b5be510b7 | ||
|
|
99ef4e6899 | ||
|
|
59b86a882e | ||
|
|
a6a729189c | ||
|
|
7829e9a3ba | ||
|
|
a2e27f3ced | ||
|
|
9175ce3646 | ||
|
|
0dbdf401eb | ||
|
|
db84078093 | ||
|
|
a0ab8069d2 | ||
|
|
ea068dc23e | ||
|
|
26756cc687 | ||
|
|
171a4e3b48 | ||
|
|
bfd5aef4d9 | ||
|
|
b3c2a48eec | ||
|
|
848cc9e43f | ||
|
|
f61230ff35 | ||
|
|
268740a142 | ||
|
|
abf8d32ff5 | ||
|
|
b769aec7e8 | ||
|
|
84832f011a | ||
|
|
b1dc766ffb | ||
|
|
3274b35468 | ||
|
|
ad76e2d3ce | ||
|
|
aec78d147a | ||
|
|
9333111a99 | ||
|
|
1f0d192fd5 | ||
|
|
21f04259cb | ||
|
|
a62eb20f26 | ||
|
|
b813a4af9d | ||
|
|
09ca411903 | ||
|
|
59c256f8cc | ||
|
|
c0e2a8a6fb | ||
|
|
c15278d3c6 | ||
|
|
7cf3c67432 | ||
|
|
1ef3796709 | ||
|
|
d3527f2aee | ||
|
|
31b3dff98e | ||
|
|
1b9cf02f1a | ||
|
|
a9963c2f43 | ||
|
|
b9bdf92065 | ||
|
|
b66b8f1880 | ||
|
|
356b211754 | ||
|
|
833389c1fc | ||
|
|
f5e30cdff3 | ||
|
|
50f6cd743a | ||
|
|
c4c6e861b5 | ||
|
|
cac293b75b | ||
|
|
2c39ba9d9e | ||
|
|
a1602fa57a | ||
|
|
f6f1adc9f4 | ||
|
|
9fbbba817e | ||
|
|
d973a8a2ca | ||
|
|
70cc044e57 | ||
|
|
f7498e5ba2 | ||
|
|
e103599984 | ||
|
|
40abe0b311 | ||
|
|
8b9e514ce9 | ||
|
|
582f3a6ae4 | ||
|
|
5589c0a80f | ||
|
|
71c67ad637 | ||
|
|
1c7aff0d5b | ||
|
|
9b359abcce | ||
|
|
67de1109fe | ||
|
|
3489567cde | ||
|
|
d69e6c3501 | ||
|
|
01dec5160e | ||
|
|
90c80c9946 | ||
|
|
0b9ca610b7 | ||
|
|
2a2782bc95 | ||
|
|
3b6b02ed19 | ||
|
|
443ae7f40b | ||
|
|
4c91ac44e1 | ||
|
|
42dc98fa45 | ||
|
|
e88bec86fb | ||
|
|
20c8ef7383 | ||
|
|
ff09db6244 | ||
|
|
c15a00d603 | ||
|
|
b18cf31bdc | ||
|
|
3ee1a96bde | ||
|
|
8c470d0e4e | ||
|
|
0826102f6e | ||
|
|
2ff52a0359 | ||
|
|
8d70999498 | ||
|
|
f6a4581dad | ||
|
|
2d74977ed3 | ||
|
|
8b06724b67 | ||
|
|
c4755fa920 | ||
|
|
36f146a71d | ||
|
|
a5515d8520 | ||
|
|
33514c6e25 | ||
|
|
d8a582aaa5 | ||
|
|
a428d33964 | ||
|
|
33639a8955 | ||
|
|
4021f07aec | ||
|
|
67bd456b63 | ||
|
|
78cc5877a3 | ||
|
|
c4bc505e03 | ||
|
|
ad372590a9 | ||
|
|
2771d526be | ||
|
|
263ec4d266 | ||
|
|
f86876328e | ||
|
|
e47167b17e | ||
|
|
5c32cf69e6 | ||
|
|
fd46017c45 | ||
|
|
95d3cca660 | ||
|
|
1fcb5560f0 | ||
|
|
06b3dc27d2 | ||
|
|
d2b683368c | ||
|
|
6b397ccd3f | ||
|
|
32006fe8e1 | ||
|
|
911e157322 | ||
|
|
100b4062bf | ||
|
|
b27e2b7a1c | ||
|
|
7085009667 | ||
|
|
4b6ceab070 | ||
|
|
49af472617 | ||
|
|
d7aeb0d3b3 | ||
|
|
35f8b775f9 | ||
|
|
079df0d6a6 | ||
|
|
36442cb3a2 | ||
|
|
217cd9183e | ||
|
|
e9c2eaebce | ||
|
|
954c790827 | ||
|
|
2d2b476688 | ||
|
|
ab58217cab | ||
|
|
6f0b05c1f6 | ||
|
|
58d35098c0 | ||
|
|
44e71e58e6 | ||
|
|
0e516bc407 | ||
|
|
9fc7b9d755 | ||
|
|
e1ff8fb520 | ||
|
|
adf308ac9f | ||
|
|
7b6abe858c | ||
|
|
6937346e5a | ||
|
|
fc1a354072 | ||
|
|
82464e4129 | ||
|
|
1a126ad743 | ||
|
|
9558194bf7 | ||
|
|
8aa2f0c018 | ||
|
|
d6de310363 | ||
|
|
1fdddb25ec | ||
|
|
a78f6f5ce3 | ||
|
|
2528727d81 | ||
|
|
97bb4dd956 | ||
|
|
3c6819c9e7 | ||
|
|
bb2fc15c07 | ||
|
|
fb129df215 | ||
|
|
050ccb921e | ||
|
|
9b9aae4c0e | ||
|
|
263569b163 | ||
|
|
2702232eea | ||
|
|
8f802b4d38 | ||
|
|
00b4aabb32 | ||
|
|
8ea84dca94 | ||
|
|
bc45b779ef | ||
|
|
c72286623f | ||
|
|
1d3c97fae5 | ||
|
|
17e22b8b93 | ||
|
|
e32714ebaf | ||
|
|
acd81548e6 | ||
|
|
2d71860fa1 | ||
|
|
6930924cc1 | ||
|
|
e5ce253daf | ||
|
|
b9cfe0e300 | ||
|
|
af91c8c9e3 | ||
|
|
08e50e3643 | ||
|
|
885001ec97 | ||
|
|
b2dae09cf1 | ||
|
|
c55d94d969 | ||
|
|
7d035f5604 | ||
|
|
a5f49080a7 | ||
|
|
8a52a85ca3 | ||
|
|
2bfdc99f89 | ||
|
|
e5da621736 | ||
|
|
359f6c455c | ||
|
|
e929af5c99 | ||
|
|
191e61400d | ||
|
|
d311b40faa | ||
|
|
7c7fc41b2c | ||
|
|
5ab0f84d41 | ||
|
|
5917b190eb | ||
|
|
828c7b814c | ||
|
|
6d3f1bbee8 | ||
|
|
85954ab7cc | ||
|
|
822b2f4ae7 | ||
|
|
ed6911a8dc | ||
|
|
d8cfe15373 | ||
|
|
dc4d6d2671 | ||
|
|
eb6b30864f | ||
|
|
1618bb1266 | ||
|
|
54e5aa28bd | ||
|
|
4e72e2ea50 | ||
|
|
134608625f | ||
|
|
11fbaa6be8 | ||
|
|
377dd08c96 | ||
|
|
6a9dfe6dd1 | ||
|
|
59da4e4f99 | ||
|
|
6d917f82d7 | ||
|
|
15780467eb | ||
|
|
1a53871517 | ||
|
|
ef0af3a494 | ||
|
|
ab854befd1 | ||
|
|
3d522c4f2e | ||
|
|
7908da3721 | ||
|
|
f9a3354741 | ||
|
|
fbf4d3b355 | ||
|
|
f1839bcfbf | ||
|
|
895af2d7ad | ||
|
|
b5510da291 | ||
|
|
3083738d00 | ||
|
|
ea84fe82dd | ||
|
|
ea8b7507e4 | ||
|
|
7dfd170983 | ||
|
|
06c7e709e6 | ||
|
|
e0f10670d7 | ||
|
|
b33d2dff4a | ||
|
|
2f1b9ebe0c | ||
|
|
9b024bda31 | ||
|
|
606dcf17c9 | ||
|
|
2fb0c29705 | ||
|
|
d673a7006b | ||
|
|
fbe3a795df | ||
|
|
0c614c3f7e | ||
|
|
ce4bebb8b4 | ||
|
|
a8b4a581e4 | ||
|
|
7d5cc80898 | ||
|
|
7fa6003156 | ||
|
|
87cdff17b8 | ||
|
|
4fbe3a3340 | ||
|
|
c2a8894d9f | ||
|
|
4bb7e224d7 | ||
|
|
c24efff92f | ||
|
|
9dc73c29ff | ||
|
|
c2d599b546 | ||
|
|
574d1d1d69 | ||
|
|
f3ff95cc57 | ||
|
|
4a2f4fdadc | ||
|
|
c2bd39a65f | ||
|
|
529b141f1b | ||
|
|
de643ee51b | ||
|
|
1dd59bc2b6 | ||
|
|
8b80bd4119 | ||
|
|
0ca0ea0e93 | ||
|
|
4f15b2f273 | ||
|
|
63e2384907 | ||
|
|
e9b66ff7fd | ||
|
|
0eff48267f | ||
|
|
4c9f247cf6 | ||
|
|
5aa7552232 | ||
|
|
0dd1a7308a | ||
|
|
315aeafd24 | ||
|
|
74fd2dcf5e | ||
|
|
da66ab2fc6 | ||
|
|
4835fba6dc | ||
|
|
c28b663f56 | ||
|
|
cc2ece53ee | ||
|
|
ccc23e6e52 | ||
|
|
cee5c16aa1 | ||
|
|
84db98515d | ||
|
|
4a24df3395 | ||
|
|
f21054f17f | ||
|
|
e12824008f | ||
|
|
0c24e04b8c | ||
|
|
e66aae1209 | ||
|
|
e652a1eced | ||
|
|
bbfbd43105 | ||
|
|
62bc9643fd | ||
|
|
a36bbb2ffa | ||
|
|
c58e2403ca | ||
|
|
926327156d | ||
|
|
45a7f9ae65 | ||
|
|
26536b965c | ||
|
|
896de29721 | ||
|
|
fdafd95584 | ||
|
|
e0f31775e5 | ||
|
|
67a67aef88 | ||
|
|
0c28152d43 | ||
|
|
a361b3ba41 | ||
|
|
20fe99a820 | ||
|
|
d380d5dd0e | ||
|
|
7df8b05e00 | ||
|
|
7708fa86bd | ||
|
|
ccb865821d | ||
|
|
7ff10f5510 | ||
|
|
fce9f83459 | ||
|
|
74609b0705 | ||
|
|
5efc10ede6 | ||
|
|
99bab479e0 | ||
|
|
70f11046f1 | ||
|
|
6094bddf6a | ||
|
|
490d571f3c | ||
|
|
18dc66f319 | ||
|
|
ba174ed435 | ||
|
|
33723056ec | ||
|
|
5f9c615ac5 | ||
|
|
76b3776d79 | ||
|
|
b3f2cddd2d | ||
|
|
d9dac047a0 | ||
|
|
a863377597 | ||
|
|
5420cd415b | ||
|
|
1d79d7fe00 | ||
|
|
fa6bc246de | ||
|
|
c9d5f9f7fa | ||
|
|
b0df2d6898 | ||
|
|
a0f4c6b6cd | ||
|
|
309775cce2 | ||
|
|
12dcdfbf4a | ||
|
|
1fc7042545 | ||
|
|
9089aba931 | ||
|
|
b12c6102c6 | ||
|
|
5548148150 | ||
|
|
aff6e4acb0 | ||
|
|
1e88014f38 | ||
|
|
5be74723d5 | ||
|
|
f0ee5ee2ae | ||
|
|
1daa34f604 | ||
|
|
258b774098 | ||
|
|
1b6f9e4f09 | ||
|
|
2598263cab | ||
|
|
98314a9a26 | ||
|
|
20074845de | ||
|
|
d0b1c816eb | ||
|
|
5f1a59f7b3 | ||
|
|
c7644e214d | ||
|
|
b6d026a2b1 | ||
|
|
23c6cb33a3 | ||
|
|
e68e8ef165 | ||
|
|
912b805777 | ||
|
|
cacdf09649 | ||
|
|
b32320d59c | ||
|
|
0e2cc687d4 | ||
|
|
1bc82611e9 | ||
|
|
6bb27b4e6f | ||
|
|
3a6d5b3e34 | ||
|
|
33ccc85fbf | ||
|
|
a21a7dbeda | ||
|
|
718dc2c691 | ||
|
|
7ca5a92e9f | ||
|
|
7a9ecfe329 | ||
|
|
ffd299f369 | ||
|
|
fbf2f1f063 | ||
|
|
66c1ca83d9 | ||
|
|
a7e542b7b2 | ||
|
|
90d73053f1 | ||
|
|
900f259620 | ||
|
|
b7367f89e2 | ||
|
|
35ba1d2ca5 | ||
|
|
cf072514a4 | ||
|
|
6133547617 | ||
|
|
d62bf49eb9 | ||
|
|
7b32dde0cf | ||
|
|
8c7e53aef8 | ||
|
|
2d9e9091b4 | ||
|
|
8363650304 | ||
|
|
21d86327b9 | ||
|
|
1e871813f8 | ||
|
|
822fb85f1c | ||
|
|
ae0e931588 | ||
|
|
6bb91df75f | ||
|
|
9c1c180619 | ||
|
|
4b27fe0f82 | ||
|
|
aab5869dc9 | ||
|
|
c6b50b2ccf | ||
|
|
ba8296ef50 | ||
|
|
5b5df2e51e | ||
|
|
73c039147e | ||
|
|
51ef78c2af | ||
|
|
42423bdf61 | ||
|
|
35e2769ba3 | ||
|
|
dc3e56d44b | ||
|
|
4d59b4b463 | ||
|
|
ae9f1f8b0d | ||
|
|
e30b9ed175 | ||
|
|
d06dbcb99d | ||
|
|
70af65c51f | ||
|
|
d288f3319a | ||
|
|
96b1ee7b66 | ||
|
|
89b37e778a | ||
|
|
a72b859f05 | ||
|
|
9d1c463247 | ||
|
|
62c7bed6bb | ||
|
|
3338ea6510 | ||
|
|
b6c015e3da | ||
|
|
547b05dcae | ||
|
|
ee7a2e692b | ||
|
|
d52161b44b | ||
|
|
9b5bf06a29 | ||
|
|
189942c45a | ||
|
|
8c68731283 | ||
|
|
ec24787b9b | ||
|
|
c220b8be8e | ||
|
|
550d8cc457 | ||
|
|
2de52008a2 | ||
|
|
4354742d3f | ||
|
|
5e1bf39839 | ||
|
|
74ab043a30 | ||
|
|
2abe265046 | ||
|
|
d4f3cd4e6c | ||
|
|
0c5a50c34e | ||
|
|
dfab29bbbc | ||
|
|
4bbbab7fa1 | ||
|
|
4c0b5d1d58 | ||
|
|
9f99e784f4 | ||
|
|
8c673d5242 | ||
|
|
6852d7e14d | ||
|
|
204808032e | ||
|
|
8952cdb813 | ||
|
|
bab3eb1e9f | ||
|
|
900bdfb199 | ||
|
|
097f404d94 | ||
|
|
f796523e3b | ||
|
|
44477d7b1a | ||
|
|
f3592d1d60 | ||
|
|
b935e20d63 | ||
|
|
0e4c8415a1 | ||
|
|
b342f14bf9 | ||
|
|
b8489059f3 | ||
|
|
ebfd86bcb9 | ||
|
|
1e37ca57bc | ||
|
|
eabd501dee | ||
|
|
252de7a589 | ||
|
|
9be2c1df5a | ||
|
|
a35c78d796 | ||
|
|
43a8a759af | ||
|
|
92abebaffa | ||
|
|
3b9b149555 | ||
|
|
8b3433b4ca | ||
|
|
b02e034621 | ||
|
|
196e484448 | ||
|
|
0cd3952d52 | ||
|
|
3640508db9 | ||
|
|
e75dcaf282 | ||
|
|
a7b6b94612 | ||
|
|
6ea0ea4619 | ||
|
|
0c71e9cf5f | ||
|
|
348fbb237b | ||
|
|
e2808ced1a | ||
|
|
30b481597b | ||
|
|
c31685b062 | ||
|
|
8dbe489d60 | ||
|
|
b11ffe9afa | ||
|
|
789da461f9 | ||
|
|
fb3d1752dc | ||
|
|
5219afb13f | ||
|
|
71e0888c65 | ||
|
|
e5456c8ca3 | ||
|
|
9987ba2fbb | ||
|
|
8ea51c3e41 | ||
|
|
fbd934d750 | ||
|
|
63d06ee32a | ||
|
|
561f1deb07 | ||
|
|
584d84475a | ||
|
|
c9e56723fb | ||
|
|
b5b6e42c2d | ||
|
|
14e56cd04f | ||
|
|
ecfbdd29a7 | ||
|
|
321a87c407 | ||
|
|
4403fa2238 | ||
|
|
f2623f0253 | ||
|
|
36f16d9f9b | ||
|
|
097d5e3f98 | ||
|
|
87267638f7 | ||
|
|
6bcc9b0cc5 | ||
|
|
40fc845461 | ||
|
|
18017c73ce | ||
|
|
9e69ae6ec0 | ||
|
|
0cb4bbcfe2 | ||
|
|
6a2f5657a5 | ||
|
|
624d84be41 | ||
|
|
8045da1f96 | ||
|
|
482f428a17 | ||
|
|
663908d17e | ||
|
|
3c61ff6895 | ||
|
|
be504fc15c | ||
|
|
60c2bd4808 | ||
|
|
03c584a5c3 | ||
|
|
3570c48420 | ||
|
|
aac5df9748 | ||
|
|
884fa08355 | ||
|
|
59dbc520b9 | ||
|
|
8075bf14ec | ||
|
|
8e0d993596 | ||
|
|
6c87713751 | ||
|
|
43ecd35402 | ||
|
|
424c89e2c8 | ||
|
|
744ccef561 | ||
|
|
7f877ee636 | ||
|
|
542d6dc061 | ||
|
|
bd1bdc03c2 | ||
|
|
b0be55ef2e | ||
|
|
6f332d8c2d | ||
|
|
cf2f488aaf | ||
|
|
8c91df4319 | ||
|
|
807eba8d98 | ||
|
|
71fb3b1313 | ||
|
|
2b9f56b456 | ||
|
|
9266e5c207 | ||
|
|
ffe2adc4c2 | ||
|
|
9b6fa32db5 | ||
|
|
fdea5e302f | ||
|
|
4fbd276fff | ||
|
|
90536704a3 | ||
|
|
f8787c67df | ||
|
|
2cea3a6585 | ||
|
|
61edffd0b6 | ||
|
|
6494ca8c26 | ||
|
|
480448ff79 | ||
|
|
8139f91db2 | ||
|
|
3d7f424b28 | ||
|
|
9e514447e6 | ||
|
|
06ed4c8da7 | ||
|
|
072c3f8be9 | ||
|
|
37b158e99b | ||
|
|
a327c0b321 | ||
|
|
d57dc285df | ||
|
|
e29cfc7b88 | ||
|
|
08c7a4033e | ||
|
|
b9dd2b7cbc | ||
|
|
e45eccd64c | ||
|
|
c93411e330 | ||
|
|
550e2d30cc | ||
|
|
48e4cfadfd | ||
|
|
c43e41c076 | ||
|
|
45af81513c | ||
|
|
5244b5c8f0 | ||
|
|
ff36b2fef1 | ||
|
|
b27997db2c | ||
|
|
b0b9a621a1 | ||
|
|
f851e70d28 | ||
|
|
09a5d1520d | ||
|
|
7ba097e34c | ||
|
|
2141e1f4da | ||
|
|
9c0fd7b027 | ||
|
|
fa9289293b | ||
|
|
3092424315 | ||
|
|
2abe327e90 | ||
|
|
47ebc2f694 | ||
|
|
9312036f69 | ||
|
|
5732b8456f | ||
|
|
7fd01b7e40 | ||
|
|
fc72dfc870 | ||
|
|
24dc14c44a | ||
|
|
1e291d4180 | ||
|
|
1af3310e0b | ||
|
|
e90c1838f1 | ||
|
|
88932bfc67 | ||
|
|
17205d39d2 | ||
|
|
9512104536 | ||
|
|
4eabdc489f | ||
|
|
fd9865b368 | ||
|
|
c5a8dca36f | ||
|
|
49d0a2aaf0 | ||
|
|
b5b941bf73 | ||
|
|
6cb39bd6ee | ||
|
|
b6b1292589 | ||
|
|
2d4e4e71ab | ||
|
|
8194d416c6 | ||
|
|
d3aa94a922 | ||
|
|
0d0ca9a73a | ||
|
|
f096e93a09 | ||
|
|
678c9b029b | ||
|
|
98d5986dde | ||
|
|
56f00fe6a6 | ||
|
|
3214bbc427 | ||
|
|
6e52046032 | ||
|
|
9d4c43f96d | ||
|
|
0ef404f3ef | ||
|
|
ac76251b75 | ||
|
|
c97610bd97 | ||
|
|
722349f586 | ||
|
|
5aabac7b4f | ||
|
|
ee2269fdf1 | ||
|
|
adb1e309e3 | ||
|
|
a5f77e7846 | ||
|
|
4711ea8c7f | ||
|
|
769529e129 | ||
|
|
5b0f1f974c | ||
|
|
cf22374926 | ||
|
|
56a52c6237 | ||
|
|
04fd44f8c2 | ||
|
|
a43729e9c1 | ||
|
|
84314598d2 | ||
|
|
9991c368cf | ||
|
|
2dfe5e6b64 | ||
|
|
57b67838f8 | ||
|
|
f4f49735be | ||
|
|
1a76549c2e | ||
|
|
02a1f4a89c | ||
|
|
cc7e6aa9ec | ||
|
|
b78838ca23 | ||
|
|
a54b6a4195 | ||
|
|
14e03bf903 | ||
|
|
dc8f2c53c9 | ||
|
|
fe1563fb03 | ||
|
|
82628607d6 | ||
|
|
1a9b178f67 | ||
|
|
db4f8ed299 | ||
|
|
0318f3f785 | ||
|
|
60914553c3 | ||
|
|
939c2c52fa | ||
|
|
f8aba65578 | ||
|
|
8fc1f7668b | ||
|
|
18ad759d43 | ||
|
|
550ca6f7dd | ||
|
|
e81829a203 | ||
|
|
257dfb8474 | ||
|
|
6a8be9661d | ||
|
|
b51bffa173 | ||
|
|
56db1b8023 | ||
|
|
6c949b18af | ||
|
|
b776827cfb | ||
|
|
951d0f322b | ||
|
|
1288670a72 | ||
|
|
1398f02865 | ||
|
|
3c3f25adce | ||
|
|
2fe0f518be | ||
|
|
a67f35e659 | ||
|
|
e071abda6e | ||
|
|
37421bd258 | ||
|
|
f2f9ac533d | ||
|
|
02a58f07a5 | ||
|
|
34eb6bf84c | ||
|
|
7e49207ad9 | ||
|
|
2f21645b39 | ||
|
|
3572c95448 | ||
|
|
3892d12a1f | ||
|
|
9973837e63 | ||
|
|
e896a79dac | ||
|
|
309b832f6c | ||
|
|
eca72d744e | ||
|
|
e63ea84226 | ||
|
|
c8322ada6f | ||
|
|
67e73fcbc3 | ||
|
|
e2f51b074e | ||
|
|
10b8ae0e74 | ||
|
|
a484cf8236 | ||
|
|
a94f97e5a2 | ||
|
|
ca48719464 | ||
|
|
adc3b832b8 | ||
|
|
ec89f0e57d | ||
|
|
65060592b4 | ||
|
|
c329dd5f20 | ||
|
|
59f918f099 | ||
|
|
f818134c2d | ||
|
|
a3fa817a37 | ||
|
|
24201fa0fb | ||
|
|
aba3421a1d | ||
|
|
ead9233560 | ||
|
|
3a09c344e5 | ||
|
|
51174a75bb | ||
|
|
0715e452fc | ||
|
|
6fdb19a86a | ||
|
|
71301ad1fe | ||
|
|
ae67a1375c | ||
|
|
b30b48f16d | ||
|
|
cc0f86ab53 | ||
|
|
6b1b061b26 | ||
|
|
87e427ccf4 | ||
|
|
35d29eb216 | ||
|
|
b2bc4cabce | ||
|
|
c8f5198c06 | ||
|
|
fb4ab34e2a | ||
|
|
6c24b618bb | ||
|
|
c0904f07e9 | ||
|
|
90f1c0a737 | ||
|
|
00893a00a4 | ||
|
|
df54432000 | ||
|
|
0d1a05edbb | ||
|
|
b5f93ed153 | ||
|
|
7aa8617ff6 | ||
|
|
3144fbf7b2 | ||
|
|
15787aaa0f | ||
|
|
5999ac2944 | ||
|
|
788a793292 | ||
|
|
4036bb6de3 | ||
|
|
c9c698da03 | ||
|
|
a76c512a1e | ||
|
|
867876c1b7 | ||
|
|
8f809a53d9 | ||
|
|
f93154cbb4 | ||
|
|
a454ded9d2 | ||
|
|
478aa72f0c | ||
|
|
8b8e49f552 | ||
|
|
3eb0e9213c | ||
|
|
b4e6089f61 | ||
|
|
924473b254 | ||
|
|
29177f5c1c | ||
|
|
309b355d41 | ||
|
|
fd05a47156 | ||
|
|
dc685bbafb | ||
|
|
5901132900 | ||
|
|
f996d024e7 | ||
|
|
33a05b7176 | ||
|
|
929811b154 | ||
|
|
47eaa65056 | ||
|
|
1bc5f606aa | ||
|
|
8c214c7d90 | ||
|
|
c1de256ae7 | ||
|
|
6362f905aa | ||
|
|
a3356cb76b | ||
|
|
8c0afd34c1 | ||
|
|
6252e73127 | ||
|
|
3aaed00349 | ||
|
|
5b2ff85187 | ||
|
|
992c25e8c8 | ||
|
|
5adb116141 | ||
|
|
05c76c6584 | ||
|
|
863863a7ac | ||
|
|
b144823cd5 | ||
|
|
1c968bda9d | ||
|
|
fb6b31106d | ||
|
|
bde0b18824 | ||
|
|
44084b4f1d | ||
|
|
29423e03de | ||
|
|
4141758ee4 | ||
|
|
0f3aeab653 | ||
|
|
a87a37ca54 | ||
|
|
cd096eacbf | ||
|
|
fd40451ef0 | ||
|
|
0845cd1982 | ||
|
|
133341c379 | ||
|
|
4a27791524 | ||
|
|
3767e0bfd2 | ||
|
|
e0c7370395 | ||
|
|
a4aa6abc9e | ||
|
|
7b5572f5de | ||
|
|
46cb52bd2b | ||
|
|
996758f6d1 | ||
|
|
4952912e16 | ||
|
|
b9366bc3f2 | ||
|
|
e21e48626d | ||
|
|
313cd61975 | ||
|
|
fb71bef6f6 | ||
|
|
717ab19c07 | ||
|
|
95bf16da60 | ||
|
|
7664faaf44 | ||
|
|
48736ea1d0 | ||
|
|
4ace088465 | ||
|
|
57b92938fb | ||
|
|
09b5605d04 | ||
|
|
1d3aaca944 | ||
|
|
1ecf3ae23d | ||
|
|
924b47635d | ||
|
|
a62da2c890 | ||
|
|
0653551d93 | ||
|
|
f581f52a6f | ||
|
|
323ce17ef1 | ||
|
|
4abe0200fb | ||
|
|
fa5c4f906f | ||
|
|
fe63a665d1 | ||
|
|
73bac1622a | ||
|
|
0b51653658 | ||
|
|
94807fd30a | ||
|
|
074d8d6c13 | ||
|
|
59f510dfa9 | ||
|
|
7828e6e284 | ||
|
|
92ef753708 | ||
|
|
62dae9a141 | ||
|
|
24c9eb7185 | ||
|
|
470bb614b9 | ||
|
|
2a52506a2c | ||
|
|
9557f2d06d | ||
|
|
947d693a15 | ||
|
|
fa58d144b9 | ||
|
|
136304ce8b | ||
|
|
16f5a6dce1 | ||
|
|
ff6d7ce7e4 | ||
|
|
1123d22446 | ||
|
|
adc39bd9fe | ||
|
|
dea7ec57f3 | ||
|
|
3ad7123361 | ||
|
|
eb1b6aad4a | ||
|
|
9dfad3b5cf | ||
|
|
4eadecc08f | ||
|
|
7875faa8fd | ||
|
|
35725c2718 | ||
|
|
c05722b074 | ||
|
|
8efe05e524 | ||
|
|
8971b0fa5a | ||
|
|
4f338b4c5f | ||
|
|
e6ae2e6f43 | ||
|
|
69bb79ebe8 | ||
|
|
8fe5ca71cc | ||
|
|
2fa094c88b | ||
|
|
84ab443080 | ||
|
|
969e27a14b | ||
|
|
6b914bae88 | ||
|
|
562235ed8e | ||
|
|
c109a0abd6 | ||
|
|
3786da3544 | ||
|
|
a537c43c80 | ||
|
|
961cadb566 | ||
|
|
c65f1b9295 | ||
|
|
c06826bea8 | ||
|
|
ea0ae16271 | ||
|
|
6260c7ab82 | ||
|
|
2afbb0b9e2 | ||
|
|
e17db42460 | ||
|
|
ef28be6ddc | ||
|
|
9f57e7afa8 | ||
|
|
6a18af92d4 | ||
|
|
bfc358e1a2 | ||
|
|
affe1f0fb4 | ||
|
|
7bdf758797 | ||
|
|
6102bda402 | ||
|
|
a3bbc66718 | ||
|
|
3aa687ee40 | ||
|
|
e2c92b03b6 | ||
|
|
27deddef1e | ||
|
|
022d7e3c4a | ||
|
|
5004da8b3c | ||
|
|
1b31d796d6 | ||
|
|
6fa54dcfa3 | ||
|
|
5fa9412f9b | ||
|
|
9ce7a6fcc8 | ||
|
|
71134c2627 | ||
|
|
709c65563b | ||
|
|
f5f88f01a7 | ||
|
|
0ec060590e | ||
|
|
74aa88fd83 | ||
|
|
ce6764bf26 | ||
|
|
c73878f849 | ||
|
|
97aa0b7b62 | ||
|
|
a81aa48e0e | ||
|
|
d416cf05c2 | ||
|
|
8be46e6ab0 | ||
|
|
fe03177a80 | ||
|
|
e9c191eb7c | ||
|
|
443d0464d1 | ||
|
|
5f817043ef | ||
|
|
d465237218 | ||
|
|
2a389c5034 | ||
|
|
e183baad7f | ||
|
|
bbefab738a | ||
|
|
93e48db1d6 | ||
|
|
23d45109e2 | ||
|
|
b0ffdfb268 | ||
|
|
1a75b85e9a | ||
|
|
6342169f5c | ||
|
|
7f0bb19134 | ||
|
|
c729873e5e | ||
|
|
82180fb512 | ||
|
|
f241cdbd15 | ||
|
|
5ca4f39da4 | ||
|
|
475561cbf5 | ||
|
|
71437f2778 | ||
|
|
1a52f6e7c0 | ||
|
|
cd8eb51131 | ||
|
|
399ede2977 | ||
|
|
5edb1e6e9a | ||
|
|
98a391165e | ||
|
|
357a9b6bfa | ||
|
|
9c727926bc | ||
|
|
e3ad881f5d | ||
|
|
5a26c18e84 | ||
|
|
aea2d9794a | ||
|
|
a3d5018c25 | ||
|
|
170c4de21f | ||
|
|
78c271834b | ||
|
|
25f418f345 | ||
|
|
3c934d03af | ||
|
|
0d56f442ae | ||
|
|
706e3fea74 | ||
|
|
b504bf0ecd | ||
|
|
bf8ac489f0 | ||
|
|
a336c5b84f | ||
|
|
76cb0d7c60 | ||
|
|
04cd790fb1 | ||
|
|
78b52bfb81 | ||
|
|
c33b770b2e | ||
|
|
b1187cbe4c | ||
|
|
acd7e4956d | ||
|
|
8c28e9a2e9 | ||
|
|
3f6a9c4981 | ||
|
|
898c972759 | ||
|
|
6ab04ba685 | ||
|
|
57f768e8ef | ||
|
|
9686f12567 | ||
|
|
9f0dd50969 | ||
|
|
5a1c1bc648 | ||
|
|
13889a2a84 | ||
|
|
52d02a9f62 | ||
|
|
0ca22719d8 | ||
|
|
3d83d7757f | ||
|
|
ef3d8224de | ||
|
|
b4933c20b2 | ||
|
|
5fb6eb9457 | ||
|
|
d53b6bf2fc | ||
|
|
ee332c38f4 | ||
|
|
07223f4c1b | ||
|
|
13c21868fb | ||
|
|
27c31ad650 | ||
|
|
6fba4d334b | ||
|
|
c42b9fa0cb | ||
|
|
6f99b2e76b | ||
|
|
4ef9574b56 | ||
|
|
36cca92666 | ||
|
|
6a3443c600 | ||
|
|
542646e71a | ||
|
|
a830e0440b | ||
|
|
f1bce10ef9 | ||
|
|
6b25822135 | ||
|
|
279f054a2c | ||
|
|
3163a4103e | ||
|
|
609400b98f | ||
|
|
5668a300e1 | ||
|
|
809f9cfe68 | ||
|
|
af6e3112d2 | ||
|
|
da3f934d35 | ||
|
|
4f52aed496 | ||
|
|
7124aa2197 | ||
|
|
bc7b8f40b5 | ||
|
|
4c804ca361 | ||
|
|
bd08536e54 | ||
|
|
eafc3d5750 | ||
|
|
f9958ef526 | ||
|
|
8a4649ceae | ||
|
|
132a2e28d9 | ||
|
|
5b1385849f | ||
|
|
db6248ad37 | ||
|
|
112ea66d7f | ||
|
|
ab3f7a0c1e | ||
|
|
5bcfd0c726 | ||
|
|
328afcbb18 | ||
|
|
ec548bdaab | ||
|
|
15f1534197 | ||
|
|
a04537d4ef | ||
|
|
f90e48bd5d | ||
|
|
31760e281b | ||
|
|
e41e098994 | ||
|
|
88b514dbff | ||
|
|
8c71c7900d | ||
|
|
54c1786435 | ||
|
|
121953dc2e | ||
|
|
6b0adf31be | ||
|
|
6f32bb0637 | ||
|
|
f18d4f0048 | ||
|
|
f5aba6a012 | ||
|
|
2d507d498d | ||
|
|
e90e6e4e71 | ||
|
|
059b0f44dd | ||
|
|
0df3a7ad55 | ||
|
|
554a1093fb | ||
|
|
4e027c715f | ||
|
|
3c9e2d9b3d | ||
|
|
27e1aaa57f | ||
|
|
29d6502c0c | ||
|
|
28241c8e55 | ||
|
|
8ab1bec608 | ||
|
|
216ca83c22 | ||
|
|
256fefac59 | ||
|
|
90f1349bd6 | ||
|
|
e4ba051b49 | ||
|
|
f61c482775 | ||
|
|
a0384118db | ||
|
|
64b1c2bbb0 | ||
|
|
a0b53946a0 | ||
|
|
b39cbe7402 | ||
|
|
2c3f195b96 | ||
|
|
d258d04042 | ||
|
|
aa7ff00380 | ||
|
|
1d6502112d | ||
|
|
901fac8f89 | ||
|
|
7afc9a0674 | ||
|
|
151f42c15d | ||
|
|
d6ef9ddc37 | ||
|
|
1e39e35251 | ||
|
|
3181cba783 | ||
|
|
ac19634172 | ||
|
|
5271ba6fbf | ||
|
|
58e389a095 | ||
|
|
4894f115e0 | ||
|
|
f3f77c9ab5 | ||
|
|
b9ebb84c0b | ||
|
|
763337e7cd | ||
|
|
527e8241a3 | ||
|
|
154966ca8d | ||
|
|
4b5d0b8232 | ||
|
|
2b87f01f90 | ||
|
|
db78c777ac | ||
|
|
85cc9924b5 | ||
|
|
ce33f8fbc7 | ||
|
|
ad63faf6ff | ||
|
|
8b5ca811dc | ||
|
|
47d988aa97 | ||
|
|
9ee76235fa | ||
|
|
492788cca1 | ||
|
|
f85c5f3969 | ||
|
|
2fbed04583 | ||
|
|
e4a7cd6ffd | ||
|
|
097d1b7ea4 | ||
|
|
5e38dc9abd | ||
|
|
47e716239e | ||
|
|
3a33cc03a0 | ||
|
|
d34f98f8db | ||
|
|
632f3b9d4e | ||
|
|
23c8107463 | ||
|
|
2f79060158 | ||
|
|
e107c41ce9 | ||
|
|
ddab271fce | ||
|
|
13428f1526 | ||
|
|
d02ce264ea | ||
|
|
367b79687e | ||
|
|
0201523656 | ||
|
|
96b0c17bc8 | ||
|
|
38fcc96c23 | ||
|
|
f10b8e1526 | ||
|
|
1d57be2fa0 | ||
|
|
353774b83f | ||
|
|
76e29a0b3d | ||
|
|
5e1537a217 | ||
|
|
368c6fdd64 | ||
|
|
b460bf2c14 | ||
|
|
d2c2b1837c | ||
|
|
66d45bf7f6 | ||
|
|
ae861da89d | ||
|
|
c38438c317 | ||
|
|
c8737b859a | ||
|
|
3448df8087 | ||
|
|
76b32e28f5 | ||
|
|
34883489c9 | ||
|
|
c32c06819d | ||
|
|
eb17935278 | ||
|
|
3914c87c71 | ||
|
|
cc3f44d5e7 | ||
|
|
9238f97b87 | ||
|
|
8fc5372c91 | ||
|
|
00cb9bde18 | ||
|
|
29408872ee | ||
|
|
fc4cf0e482 | ||
|
|
6e60f4b0dd | ||
|
|
440177eaa5 | ||
|
|
ce09e3e287 | ||
|
|
40b61379fa | ||
|
|
69329ce86d | ||
|
|
eff45a28a4 | ||
|
|
d721a49ab4 | ||
|
|
96d31fc723 | ||
|
|
270e0b5da8 | ||
|
|
8f23ba8872 | ||
|
|
260671922b | ||
|
|
4c3fe87a5f | ||
|
|
bb912ca149 | ||
|
|
98e0ae0f05 | ||
|
|
7813210078 | ||
|
|
c51ee20010 | ||
|
|
d11c09b3de | ||
|
|
4a723d4abb | ||
|
|
eb42948fa0 | ||
|
|
481970fd65 | ||
|
|
a9b84ac58d | ||
|
|
4be8d83314 | ||
|
|
a7489e8679 | ||
|
|
593439faf5 | ||
|
|
6500b9e510 | ||
|
|
a9f055dc21 | ||
|
|
0d78f5db73 | ||
|
|
429575f5f8 | ||
|
|
36c6d3418e | ||
|
|
b3ba614d9f | ||
|
|
587e836031 | ||
|
|
747a13ac8c | ||
|
|
2ef4bc40a5 | ||
|
|
9570399ab9 | ||
|
|
ee68d9b149 | ||
|
|
645ac3187a | ||
|
|
765e9a4e08 | ||
|
|
3c33a6900e | ||
|
|
0424a3869f | ||
|
|
a17716f2e4 | ||
|
|
ac39d3385b | ||
|
|
520a386717 | ||
|
|
acda86eedf | ||
|
|
c865af543b | ||
|
|
1739602bdf | ||
|
|
e2a5e5c021 | ||
|
|
201d5a6f86 | ||
|
|
13bca95e3c | ||
|
|
54d4ceadd1 | ||
|
|
05be407f37 | ||
|
|
8b23ece688 | ||
|
|
460bc17e36 | ||
|
|
70fdbcdb66 | ||
|
|
7b8a202131 | ||
|
|
b38a7f49fd | ||
|
|
ae384e035b | ||
|
|
25c43247cf | ||
|
|
5e6d6f0d2c | ||
|
|
883c0696e7 | ||
|
|
2d7fc4deb5 | ||
|
|
e1008951b5 | ||
|
|
659382d41b | ||
|
|
41a0e0f205 | ||
|
|
098b830a08 | ||
|
|
ce05a79574 | ||
|
|
d41000fb8d | ||
|
|
e93bf6a121 | ||
|
|
21427aa533 | ||
|
|
ba397bbff9 | ||
|
|
49331ccf19 | ||
|
|
80498f7e66 | ||
|
|
c5128990bd | ||
|
|
5e700de7fd | ||
|
|
380ea308b3 | ||
|
|
0bbdd3eeeb | ||
|
|
ff98192d92 | ||
|
|
25c832ffae | ||
|
|
da75d733d5 | ||
|
|
03fd7c33ce | ||
|
|
a0e169e92c | ||
|
|
ae6fa8e7b2 | ||
|
|
bf20ea5647 | ||
|
|
6f4dd9b457 | ||
|
|
03a2e9789d | ||
|
|
b0f8f9ebc3 | ||
|
|
fb47dd5c88 | ||
|
|
36ee900d8a | ||
|
|
fde6f0d0af | ||
|
|
168c61e4b0 | ||
|
|
cc38f1482c | ||
|
|
83392b81a5 | ||
|
|
4617b50bf0 | ||
|
|
0cc2787d3c | ||
|
|
3796cff48d | ||
|
|
350b09a77c | ||
|
|
28c3bb7053 | ||
|
|
66fa46f937 | ||
|
|
e639827cbe | ||
|
|
45e992d65b | ||
|
|
ebcd11aae9 | ||
|
|
b65373b837 | ||
|
|
9dbc09cccd | ||
|
|
3ba5b6b727 | ||
|
|
137a714e06 | ||
|
|
cc8f94fcff | ||
|
|
4e6fd62f62 | ||
|
|
29dfd29937 | ||
|
|
a7cc9b4145 | ||
|
|
c3fc12f773 | ||
|
|
6056b05b0f | ||
|
|
b395320ffd | ||
|
|
6cae742920 | ||
|
|
37997583c9 | ||
|
|
990f05bc7d | ||
|
|
e130611866 | ||
|
|
c3fe512dac | ||
|
|
8443713d48 | ||
|
|
b850065299 | ||
|
|
58720634cc | ||
|
|
ddd853d5dc | ||
|
|
09c1bfa20a | ||
|
|
0cfd2ba284 | ||
|
|
a32e4bb3a4 | ||
|
|
56905d3187 | ||
|
|
b7e83c7fd6 | ||
|
|
8b6afcefd5 | ||
|
|
9e71085823 | ||
|
|
d5ea944357 | ||
|
|
483abee1ae | ||
|
|
244224ac9b | ||
|
|
0916597990 | ||
|
|
685b5abe03 | ||
|
|
1535fc69dc | ||
|
|
d7a6260e2c | ||
|
|
584cf3bcea | ||
|
|
2c5be3a77e | ||
|
|
5de121a000 | ||
|
|
bd8e80510e | ||
|
|
b235016207 | ||
|
|
5befe9f1d7 | ||
|
|
26b50243af | ||
|
|
70a3e0e500 | ||
|
|
97dbbd22b0 | ||
|
|
4bc8741dbc | ||
|
|
20ade30be8 | ||
|
|
5a8bcaf0ac | ||
|
|
87dfcb8ed0 | ||
|
|
f54e9e9e5d | ||
|
|
28e75dbfbc | ||
|
|
407c4a499d | ||
|
|
9fe3b80fbf | ||
|
|
ef0fbccb12 | ||
|
|
7a0921765a | ||
|
|
ee2d752e25 | ||
|
|
5363501c98 | ||
|
|
aedb19901b | ||
|
|
44a1103210 | ||
|
|
78a1c8d249 | ||
|
|
495704cf41 | ||
|
|
4698605a70 | ||
|
|
2b0a31008b | ||
|
|
4c4db724a5 | ||
|
|
c4f5c234ae | ||
|
|
70034bab61 | ||
|
|
eed54ed526 | ||
|
|
7728658175 | ||
|
|
3f133c868d | ||
|
|
029d26b5af | ||
|
|
d0084862b7 | ||
|
|
5ded7d579a | ||
|
|
758033a230 | ||
|
|
e8ae4e306f | ||
|
|
a3318986d7 | ||
|
|
1c98d1602b | ||
|
|
2db5177e23 | ||
|
|
ebdb9f8979 | ||
|
|
fe4c69e3a9 | ||
|
|
d961797c40 | ||
|
|
b8dd8d30a6 | ||
|
|
213fb80f95 | ||
|
|
7531eb4d64 | ||
|
|
f7c9d08b6c | ||
|
|
31995613ba | ||
|
|
529df15375 | ||
|
|
abfbf9959a | ||
|
|
9caa1e10cc | ||
|
|
81139ab8b2 | ||
|
|
5f0510484e | ||
|
|
c87b3a97d7 | ||
|
|
74712cf2d9 | ||
|
|
40e0776219 | ||
|
|
dc1d3ec5da | ||
|
|
d72bc81563 | ||
|
|
4d3bddc4d2 | ||
|
|
3cdbeaa768 | ||
|
|
13a9ca2ff3 | ||
|
|
53f6c18cbe | ||
|
|
3211037aba | ||
|
|
f5bf5afe14 | ||
|
|
4f65bbc723 | ||
|
|
56e8e3862b | ||
|
|
0d53e23151 | ||
|
|
dcdc372e45 | ||
|
|
ca847752df | ||
|
|
6156dc0824 | ||
|
|
04374dccdf | ||
|
|
1e4e67cc37 | ||
|
|
338df4aa44 | ||
|
|
fc24702844 | ||
|
|
fb82f16ef6 | ||
|
|
d915c646bb | ||
|
|
400ec40ff1 | ||
|
|
d73f64b669 | ||
|
|
403a160917 | ||
|
|
3904e6d120 | ||
|
|
bcdc2e793e | ||
|
|
cc68ee3d27 | ||
|
|
0b0ad5067c | ||
|
|
b391d4496c | ||
|
|
31df77a0b9 | ||
|
|
5e053d2954 | ||
|
|
3cfa02fde2 | ||
|
|
e3a5ac1432 | ||
|
|
97abf1da76 | ||
|
|
1a28d142f2 | ||
|
|
6dcd0dc8cc | ||
|
|
23d812cdd4 | ||
|
|
90270ad31f | ||
|
|
5ed8d90c70 | ||
|
|
1f2182d014 | ||
|
|
c1a2b819ec | ||
|
|
2b8e3fda63 | ||
|
|
8bb0988010 | ||
|
|
5c4eaa17cf | ||
|
|
4bb44a8d1a | ||
|
|
826418d5e4 | ||
|
|
c2810be1b8 | ||
|
|
3243d57b01 | ||
|
|
9bf1637320 | ||
|
|
ae415033dd | ||
|
|
41d2743816 | ||
|
|
2e6eb810cd | ||
|
|
7f99ef2c74 | ||
|
|
fda79460b8 | ||
|
|
b91155c2ce | ||
|
|
83e2b0af7f | ||
|
|
d82b3dc896 | ||
|
|
7f732bcfef | ||
|
|
32a6c46e8a | ||
|
|
097615ef69 | ||
|
|
ebaddd76a7 | ||
|
|
aa4105e0cd | ||
|
|
487e0bf370 | ||
|
|
8121c489d5 | ||
|
|
3362bbeb45 | ||
|
|
08796da55e | ||
|
|
b68b17c3df | ||
|
|
218605334c | ||
|
|
8d9f9bead8 | ||
|
|
86e1c292f2 | ||
|
|
b4c334ed9c | ||
|
|
3b3c23451b | ||
|
|
8f71f77b1b | ||
|
|
84f53fd682 | ||
|
|
a3a27259fc | ||
|
|
eff6c8c351 | ||
|
|
2d21c21256 | ||
|
|
cf01a524e5 | ||
|
|
51e2f71afc | ||
|
|
358781905f | ||
|
|
6b782ede62 | ||
|
|
425080093b | ||
|
|
415b20020a | ||
|
|
29b36472c9 | ||
|
|
5f6bd2d993 | ||
|
|
8fb6d9152f | ||
|
|
1884048f9c | ||
|
|
1d859d3fb9 | ||
|
|
b41e78e71e | ||
|
|
e8afbd85a9 | ||
|
|
d9a8fc8e8b | ||
|
|
a15b65f819 | ||
|
|
7dc35d143a | ||
|
|
0bdb22e934 | ||
|
|
09b777181a | ||
|
|
18217c58b6 | ||
|
|
147926bcf1 | ||
|
|
3c904fc048 | ||
|
|
d1736d9f09 | ||
|
|
8ef1fd811b | ||
|
|
a64cfa6307 | ||
|
|
3f80845371 | ||
|
|
80382199ee | ||
|
|
292e9ec0ae | ||
|
|
59b57e268d | ||
|
|
153e68f7ae | ||
|
|
0b3d36cee0 | ||
|
|
ce2557dd24 | ||
|
|
a8f5835a39 | ||
|
|
9a273473d5 | ||
|
|
25b0c19db1 | ||
|
|
bbc3e7f3aa | ||
|
|
8053591031 | ||
|
|
3d2fcd020d | ||
|
|
3d7fd68fa8 | ||
|
|
1a011d9ea4 | ||
|
|
57a943edec | ||
|
|
85e9dc53f2 | ||
|
|
a682d8f7df | ||
|
|
bdc1073a28 | ||
|
|
ac79c57de4 | ||
|
|
1b70e45a1c | ||
|
|
97d0c470fb | ||
|
|
c92589da2a | ||
|
|
3dffd231ae | ||
|
|
f429bdef8e | ||
|
|
11f223ab04 | ||
|
|
2ccf407bd9 | ||
|
|
a172d476f5 | ||
|
|
f37303e8a0 | ||
|
|
60016fd532 | ||
|
|
ab3de1f60b | ||
|
|
6014509dd2 | ||
|
|
4e856ea783 | ||
|
|
23aeefbcbd | ||
|
|
b84afb99a5 | ||
|
|
2dada15292 | ||
|
|
a87e2199f0 | ||
|
|
0e85c73a77 | ||
|
|
d7ef54a37e | ||
|
|
ed47a5e3c1 | ||
|
|
27698ebea0 | ||
|
|
3bcdcd9284 | ||
|
|
640379e4d7 | ||
|
|
47b4dfb4b8 | ||
|
|
da2ea1cd1b | ||
|
|
12e80d60b6 | ||
|
|
141fd8320e | ||
|
|
a9ac930cc0 | ||
|
|
344f06178f | ||
|
|
0f03f4fffd | ||
|
|
24ef816c2e | ||
|
|
516e02be1d | ||
|
|
3c13551a7a | ||
|
|
efcf05ad8b | ||
|
|
26b7e1c5c3 | ||
|
|
2e71180f7c | ||
|
|
a42e0db899 | ||
|
|
3c7dda7118 | ||
|
|
740d8aa706 | ||
|
|
e82a1c2b81 | ||
|
|
7ea7795913 | ||
|
|
2589aa1da9 | ||
|
|
18295e83fb | ||
|
|
768c4561a9 | ||
|
|
dcb7e7715f | ||
|
|
767b20f059 | ||
|
|
66fba5f20a | ||
|
|
921186e2e0 | ||
|
|
5d41b6908c | ||
|
|
fede8d633a | ||
|
|
182f49f758 | ||
|
|
91c914fd14 | ||
|
|
c6ea0a85cb | ||
|
|
0a6e0a100c | ||
|
|
d4b81c4cdb | ||
|
|
2b4f67befe | ||
|
|
56597bbd29 | ||
|
|
c0fc874ce4 | ||
|
|
d26ab69b18 | ||
|
|
346148ae60 | ||
|
|
8e9bcc6040 | ||
|
|
f4c76467bb | ||
|
|
26a8d8511f | ||
|
|
1873dc6a20 | ||
|
|
8fa94d8452 | ||
|
|
d62088fa1f | ||
|
|
03d8e2e0e8 | ||
|
|
4397e0c756 | ||
|
|
42b23f6e3a | ||
|
|
501b0719cc | ||
|
|
1a7289a395 | ||
|
|
6153db71c8 | ||
|
|
5a7eae76d7 | ||
|
|
cc8e5da17b | ||
|
|
54d44ba87a | ||
|
|
64c46e3941 | ||
|
|
2458539c6f | ||
|
|
c56b9a0573 | ||
|
|
eeb00763d3 | ||
|
|
800a484acc | ||
|
|
b3e444be47 | ||
|
|
ff025b94d2 | ||
|
|
96986e8a22 | ||
|
|
2d0c72d5bb | ||
|
|
384ff40d88 | ||
|
|
8dca28adeb | ||
|
|
1f6a95c4a7 | ||
|
|
3493f16035 | ||
|
|
97d12be54d | ||
|
|
19d84bb479 | ||
|
|
2a29e3194e | ||
|
|
4b8904f335 | ||
|
|
a9d43435e9 | ||
|
|
9884a767ff | ||
|
|
aa1dba6241 | ||
|
|
ba9293a598 | ||
|
|
7a15e55da2 | ||
|
|
77a851cbe0 | ||
|
|
595eaa7a20 | ||
|
|
a66b6d019e | ||
|
|
b6257aa21c | ||
|
|
1a46c7f001 | ||
|
|
8ddbe18543 | ||
|
|
5c8de33eb6 | ||
|
|
7c663cacf8 | ||
|
|
1bedf8394d | ||
|
|
df0ca8b740 | ||
|
|
ced188f286 | ||
|
|
96fa6b5507 | ||
|
|
c19600bc7c | ||
|
|
cbddefcda2 | ||
|
|
a06d2f4c81 | ||
|
|
929fc80e66 | ||
|
|
56f2f1053b | ||
|
|
3f1ec34dea | ||
|
|
ddc4f9d452 | ||
|
|
14ae10536f | ||
|
|
a9a3863d05 | ||
|
|
9e8ebe17ea | ||
|
|
9cca472d29 | ||
|
|
44390a6280 | ||
|
|
345cdb2702 | ||
|
|
a2adf76962 | ||
|
|
3d477f6296 | ||
|
|
b0469ed5bc | ||
|
|
63f5b92a3c | ||
|
|
a5d03df940 | ||
|
|
2b07bcbca7 | ||
|
|
d55bfd6495 | ||
|
|
8210891675 | ||
|
|
beeeadf34a | ||
|
|
1f8b2098cc | ||
|
|
669ebf79e1 | ||
|
|
8a0077c94d | ||
|
|
a2910deed3 | ||
|
|
039623097d | ||
|
|
833158b0de | ||
|
|
d1598d61ac | ||
|
|
672149d3f7 | ||
|
|
5db4c5ced2 | ||
|
|
8f07621946 | ||
|
|
3235e60f72 | ||
|
|
566640d287 | ||
|
|
c10686e546 | ||
|
|
fb1ac61d64 | ||
|
|
813bc84575 | ||
|
|
323e9d7a20 | ||
|
|
f134a0288e | ||
|
|
7324f15c50 | ||
|
|
b8286e7875 | ||
|
|
1afb66ce0b | ||
|
|
ec4670757e | ||
|
|
92a9e7cb78 | ||
|
|
dd8e8274a1 | ||
|
|
b010996391 | ||
|
|
5bbe2b9d73 | ||
|
|
c2336d9ca8 | ||
|
|
1b29571ba9 | ||
|
|
810d2e3924 | ||
|
|
55d24994df | ||
|
|
f0bba4b77b | ||
|
|
eefc7fd153 | ||
|
|
f24b2e5040 | ||
|
|
d96c180631 | ||
|
|
f78933e2a4 | ||
|
|
b30b996e9c | ||
|
|
a51cb49225 | ||
|
|
f4924c3140 | ||
|
|
c1b08383ef | ||
|
|
59651ee24d | ||
|
|
11b79b2b48 | ||
|
|
ac03db6380 | ||
|
|
5d590d598e | ||
|
|
17a157eb60 | ||
|
|
2446a94d50 | ||
|
|
53240b6360 | ||
|
|
b4b09213f7 | ||
|
|
cdf6fd07cf | ||
|
|
a0c5748c59 | ||
|
|
d599c63816 | ||
|
|
c4fb834cd0 | ||
|
|
f605cf7e85 | ||
|
|
fa06b7148e | ||
|
|
f17755aad8 | ||
|
|
e4fdab0a19 | ||
|
|
94af31c686 | ||
|
|
28902b4c9f | ||
|
|
e57ed73f98 | ||
|
|
bf1eb5985a | ||
|
|
2188ce8a4e | ||
|
|
33767a590a | ||
|
|
352469b662 | ||
|
|
01b8da7cc2 | ||
|
|
f9b517c849 | ||
|
|
962418a9ee | ||
|
|
3353d57c84 | ||
|
|
0ed7db3b0c | ||
|
|
5ad58e5b25 | ||
|
|
e28f174f20 | ||
|
|
4a8aae7aff | ||
|
|
e780a863cf | ||
|
|
86c2724ceb | ||
|
|
6677cf39a9 | ||
|
|
0bac1ae3c4 | ||
|
|
a152d8bf06 | ||
|
|
badba682a0 | ||
|
|
90ae9e2a7e | ||
|
|
69cbede2f4 | ||
|
|
754c3d7e75 | ||
|
|
3814a2d3e9 | ||
|
|
97fb5678bc | ||
|
|
964249c1d4 | ||
|
|
607ae55db7 | ||
|
|
b7dae29b5f | ||
|
|
87d48a3428 | ||
|
|
82a8876209 | ||
|
|
325ad1914b | ||
|
|
d1bcef4df7 | ||
|
|
7c0ef24bfd | ||
|
|
038c3610ac | ||
|
|
62ea562f26 | ||
|
|
3f17e9b811 | ||
|
|
c08c077b8f | ||
|
|
b5efc28b0f | ||
|
|
ab55a5fe19 | ||
|
|
f5e493e5dc | ||
|
|
e6ab5eba07 | ||
|
|
d0f1b621fe | ||
|
|
79aca06be8 | ||
|
|
9e7f2e22dd | ||
|
|
d11c9c83f0 | ||
|
|
9e0622f91f | ||
|
|
ea7e30b9cc | ||
|
|
9c52ea8eca | ||
|
|
79db2920ca | ||
|
|
df9370a37f | ||
|
|
416d07fcce | ||
|
|
5384a32d58 | ||
|
|
18e9679553 | ||
|
|
c0cef1b178 | ||
|
|
045c1833e3 | ||
|
|
d4f8134de4 | ||
|
|
78596eb4c9 | ||
|
|
380a275248 | ||
|
|
a0b98cabf1 | ||
|
|
2ccc6dd141 | ||
|
|
a52336dc01 | ||
|
|
d70ae69dae | ||
|
|
5eeed269b6 | ||
|
|
eedc9e8f94 | ||
|
|
950af7b268 | ||
|
|
5f9e02012a | ||
|
|
548517b906 | ||
|
|
c29f902483 | ||
|
|
46919ba5a3 | ||
|
|
c4a4f33633 | ||
|
|
5176f0456f | ||
|
|
fb3e2635f5 | ||
|
|
380d1269d0 | ||
|
|
069e82c097 | ||
|
|
9a836a01fd | ||
|
|
eaffa01452 | ||
|
|
169fa21966 | ||
|
|
20e0dae7a3 | ||
|
|
358db187be | ||
|
|
fe17e3c4bb | ||
|
|
60e0d3c40c | ||
|
|
1753a6f654 | ||
|
|
d41e0e6c8c | ||
|
|
6f09d91a50 | ||
|
|
75c6008acc | ||
|
|
7c581cc7bb | ||
|
|
0715f008ca | ||
|
|
61f4c3760b | ||
|
|
ad1a464335 | ||
|
|
76ca9a1153 | ||
|
|
461a786ebc | ||
|
|
170c282f3e | ||
|
|
c7480c81b2 | ||
|
|
de62a0c11b | ||
|
|
dc35f63586 | ||
|
|
f5d7808792 | ||
|
|
1bf03b619a | ||
|
|
e109f5782c | ||
|
|
3c57cf0f65 | ||
|
|
475917c110 | ||
|
|
3b7ece1790 | ||
|
|
2a6c5faa79 | ||
|
|
7446cce387 | ||
|
|
15bb477e84 | ||
|
|
83071f3a1d | ||
|
|
eb579823db | ||
|
|
fe702bcaea | ||
|
|
0d87a6ea93 | ||
|
|
d9135c134b | ||
|
|
eb33b0c3c7 | ||
|
|
93471f4342 | ||
|
|
0db0bfdfd4 | ||
|
|
92e4dbcaa8 | ||
|
|
15443d155b | ||
|
|
21283962c4 | ||
|
|
1816aa2761 | ||
|
|
3d862b03eb | ||
|
|
411fca2ab3 | ||
|
|
8190907e39 | ||
|
|
05fbc0b80d | ||
|
|
347397f4da | ||
|
|
1d0e37950d | ||
|
|
7828df20fc | ||
|
|
a72f6b964f | ||
|
|
950dc05843 | ||
|
|
6a242635f7 | ||
|
|
dff1bd0d40 | ||
|
|
d678390b17 | ||
|
|
579b0bc5ef | ||
|
|
23b01416d7 | ||
|
|
3266b33b05 | ||
|
|
ad686553e5 | ||
|
|
e289f5acbd | ||
|
|
1573f73a0e | ||
|
|
efcac585ef | ||
|
|
89757bc7dd | ||
|
|
04e8391ca2 | ||
|
|
7f6bda6c76 | ||
|
|
b3b5063b50 | ||
|
|
a79d9aa094 | ||
|
|
b8f3200f8f | ||
|
|
b702566cb0 | ||
|
|
69b4d0a322 | ||
|
|
b2d17e3639 | ||
|
|
29ef585c31 | ||
|
|
d3d1c4ccb9 | ||
|
|
f53a260dff | ||
|
|
e8e86d1bf4 | ||
|
|
284d8f07fa | ||
|
|
aca1d26a76 | ||
|
|
3ef5c28bac | ||
|
|
de5c9f6047 | ||
|
|
b4b93af96a | ||
|
|
9d52e8853e | ||
|
|
2369c9b3be | ||
|
|
8ee0699b3e | ||
|
|
fcb18e56f0 | ||
|
|
14bbab0969 | ||
|
|
d84fe65460 | ||
|
|
047b8d1433 | ||
|
|
6e699a27ab | ||
|
|
fd45d20876 | ||
|
|
f977ff30ea | ||
|
|
27946aabae | ||
|
|
acdc935f70 | ||
|
|
28cd928a1b | ||
|
|
405f09ff00 | ||
|
|
9c1ac14bf1 | ||
|
|
920afd6045 | ||
|
|
60912da17c | ||
|
|
68eff16f64 | ||
|
|
475eb0565d | ||
|
|
cc58b6739b | ||
|
|
3f66a76727 | ||
|
|
deaec751b0 | ||
|
|
baff0ab7e4 | ||
|
|
45a56f6ca3 | ||
|
|
8a14756cea | ||
|
|
c39b0f1e02 | ||
|
|
27d8706023 | ||
|
|
ef434e5491 | ||
|
|
88c960dfad | ||
|
|
d028dee724 | ||
|
|
d17a9e2ef1 | ||
|
|
85399792fb | ||
|
|
a02ed17233 | ||
|
|
3dc5f1c843 | ||
|
|
2a077ecf81 | ||
|
|
1ae5fe8cc3 | ||
|
|
658ac6b759 | ||
|
|
08973f9b50 | ||
|
|
1abd3e2aa7 | ||
|
|
49929dd4a0 | ||
|
|
15cce31886 | ||
|
|
0302556635 | ||
|
|
1ae8ff4b99 | ||
|
|
ca75a702ca | ||
|
|
14096aeaf8 | ||
|
|
b9ebef7817 | ||
|
|
b4f6f8f2d0 | ||
|
|
d27b2f9727 | ||
|
|
fd0e58e957 | ||
|
|
eaf08c60d4 | ||
|
|
6e900d111c | ||
|
|
cf6a192ccc | ||
|
|
515a8d2b54 | ||
|
|
4018604028 | ||
|
|
55d70f8454 | ||
|
|
fd08b5d4ee | ||
|
|
f7cd18d4ad | ||
|
|
078e098b28 | ||
|
|
b2f761ef76 | ||
|
|
cdd894b50c | ||
|
|
323524ac1b | ||
|
|
bc04ca3db7 | ||
|
|
434d2c76e0 | ||
|
|
009aecc72a | ||
|
|
ddd7885038 | ||
|
|
a46a7f5429 | ||
|
|
fe5e7011cc | ||
|
|
e12da9e82d | ||
|
|
dddf4d12df | ||
|
|
96033cbc78 | ||
|
|
b821d3fd09 | ||
|
|
1b6dcb00e7 | ||
|
|
938e4e1ad7 | ||
|
|
cd99b04507 | ||
|
|
ba49cde9f6 | ||
|
|
3f82e25074 | ||
|
|
f132f88059 | ||
|
|
38ff9e748a | ||
|
|
c299e0c3e2 | ||
|
|
d4127889c3 | ||
|
|
994ead0e3e | ||
|
|
3e5900b1e0 | ||
|
|
f579c43b29 | ||
|
|
f2fee6779d | ||
|
|
4abb7ecc46 | ||
|
|
7e8fa56bf5 | ||
|
|
ce02ba86db | ||
|
|
7fcb585268 | ||
|
|
e143280c6c | ||
|
|
f82d36afb0 | ||
|
|
e14d899ac4 | ||
|
|
6a16cbabd2 | ||
|
|
e52711d592 | ||
|
|
abc01538f9 | ||
|
|
6eaf4359b5 | ||
|
|
728959332b | ||
|
|
d58a0981c4 | ||
|
|
23cdcffb12 | ||
|
|
944363765a | ||
|
|
ace3bdbb8f | ||
|
|
7926b9fd0f | ||
|
|
4617b34478 | ||
|
|
7fcda1f0e7 | ||
|
|
a72842cafe | ||
|
|
8ffbfd5653 | ||
|
|
955681886f | ||
|
|
066656adf8 | ||
|
|
a5db2a49a8 | ||
|
|
4a5dd111d7 | ||
|
|
73d260c2db | ||
|
|
c86e895ea9 | ||
|
|
208bdb28fe | ||
|
|
70b3b2f75b | ||
|
|
8f35603493 | ||
|
|
1c592e6713 | ||
|
|
b0d22fc035 | ||
|
|
7f5c9dd4ef | ||
|
|
76e2a3ef46 | ||
|
|
9c48b0054f | ||
|
|
72db97df79 | ||
|
|
a8b8b49dce | ||
|
|
26f9939bff | ||
|
|
37ec94c157 | ||
|
|
b336b2e54e | ||
|
|
91a0cecffa | ||
|
|
a59204cd99 | ||
|
|
cd61c3fd8d | ||
|
|
34082cb122 | ||
|
|
70d92cef40 | ||
|
|
b3c1a97f3d | ||
|
|
f3aa63aa13 | ||
|
|
27c8753b3a | ||
|
|
d352007cd2 | ||
|
|
3fe95f4362 | ||
|
|
f13a201c56 | ||
|
|
d4ca8c77b5 | ||
|
|
920690e504 | ||
|
|
282eebce76 | ||
|
|
abeddf8c65 | ||
|
|
826cafc3a9 | ||
|
|
04422376f2 | ||
|
|
f88f805f28 | ||
|
|
bfe4ec2818 | ||
|
|
9e07d636a8 | ||
|
|
b10211dcf9 | ||
|
|
9a7c88efbf | ||
|
|
a0750dddfc | ||
|
|
90b0100048 | ||
|
|
bbb5763b8c | ||
|
|
b029070d94 | ||
|
|
499b2dc748 | ||
|
|
153f1cbd31 | ||
|
|
9247f96679 | ||
|
|
d18d138f73 | ||
|
|
36f0686cf4 | ||
|
|
963e637931 | ||
|
|
570d5bc60b | ||
|
|
4c37af4976 | ||
|
|
f09fbdba72 | ||
|
|
17cd4b0460 | ||
|
|
02e234a627 | ||
|
|
79c13c7f31 | ||
|
|
c86ae98d04 | ||
|
|
aec129f6bb | ||
|
|
b7d6e06530 | ||
|
|
d1fc654e36 | ||
|
|
1e99dbe589 | ||
|
|
80a5f6188b | ||
|
|
265728dc38 | ||
|
|
863a29dbe6 | ||
|
|
b28330ed56 | ||
|
|
4ec7666985 | ||
|
|
63cf643300 | ||
|
|
85c167a5f4 | ||
|
|
1f4fbbd5f8 | ||
|
|
d9405428d6 | ||
|
|
91280c2177 | ||
|
|
9abba00fe1 | ||
|
|
cd2b36e5c7 | ||
|
|
8552d7e825 | ||
|
|
5e1e9399cc | ||
|
|
6a5e4af0f3 | ||
|
|
38b31fce3c | ||
|
|
c5fa140d07 | ||
|
|
e13f1cf73a | ||
|
|
03ff3f7257 | ||
|
|
4c990f43a7 | ||
|
|
75497aa031 | ||
|
|
a8c69d1cec | ||
|
|
dc4c1b9b2f | ||
|
|
1ef654578e | ||
|
|
a610fe2fd2 | ||
|
|
97cbb8a03f | ||
|
|
0e28127851 | ||
|
|
c7c41805d8 | ||
|
|
6cef45d3bd | ||
|
|
ded05a5408 | ||
|
|
361a4cc242 | ||
|
|
6cfed6c229 | ||
|
|
e8d62ab190 | ||
|
|
7a027c4149 | ||
|
|
425de91c57 | ||
|
|
d50aef3832 | ||
|
|
afe2b1b8ce | ||
|
|
566fff6c0c | ||
|
|
88b9c926c4 | ||
|
|
2db13a7ec1 | ||
|
|
4d6b740f30 | ||
|
|
9a8025777b | ||
|
|
715bc2ecbd | ||
|
|
e05c456f99 | ||
|
|
3c4459bbac | ||
|
|
cd128c5683 | ||
|
|
b4cfec133d | ||
|
|
408c5351bf | ||
|
|
6fd295211b | ||
|
|
6e9db6d7c9 | ||
|
|
cd4964401f | ||
|
|
7572febd8d | ||
|
|
90180b890d | ||
|
|
1e6b0ac6a9 | ||
|
|
6ab9b5fba7 | ||
|
|
b9cbf749be | ||
|
|
ea7de1c085 | ||
|
|
eaa681dfc3 | ||
|
|
2d79c3df9f | ||
|
|
649134bd22 | ||
|
|
abf3192476 | ||
|
|
b7a2312994 | ||
|
|
4d6d16a58b | ||
|
|
4d389dcfa3 | ||
|
|
01fa48df18 | ||
|
|
3fedf57b72 | ||
|
|
3140c2e90c | ||
|
|
5fbab18684 | ||
|
|
1fc7c4d53d | ||
|
|
2f14216e49 | ||
|
|
ae602a2f6f | ||
|
|
08c701a3eb | ||
|
|
40cebf61c2 | ||
|
|
c6fa18fe8d | ||
|
|
3598d5efdf | ||
|
|
0b305ec22f | ||
|
|
fe694de119 | ||
|
|
74bb03f92e | ||
|
|
3446abd7d8 | ||
|
|
ed0cc6c9be | ||
|
|
ef8d6ddf07 | ||
|
|
3a3dd05393 | ||
|
|
f6417a3bd5 | ||
|
|
49df4425ea | ||
|
|
758129d035 | ||
|
|
608e6e7a1d | ||
|
|
6cffdba578 | ||
|
|
9b64cc270a | ||
|
|
cc5526f7f3 | ||
|
|
77ed94a2e8 | ||
|
|
54657e5dc7 | ||
|
|
62bc3dd614 | ||
|
|
ec7f10d119 | ||
|
|
50f22ccf25 | ||
|
|
9ae4a5053a | ||
|
|
807bddeace | ||
|
|
74ce6ca353 | ||
|
|
cb10380498 | ||
|
|
490f850834 | ||
|
|
9bd75acdba | ||
|
|
e8e8bd3262 | ||
|
|
55411f7979 | ||
|
|
40e8d39f75 | ||
|
|
bd81680cdb | ||
|
|
fa557969df | ||
|
|
2a5f92eaf2 | ||
|
|
7cd5dd9929 | ||
|
|
a6f18efbc7 | ||
|
|
fedaa33741 | ||
|
|
1eadd06f9d | ||
|
|
4f1c2731bf | ||
|
|
00b52794aa | ||
|
|
f9c2799526 | ||
|
|
bd1278aab4 | ||
|
|
4f8063214f | ||
|
|
fcdf2fba0e | ||
|
|
a8479fb315 | ||
|
|
9fc473efb1 | ||
|
|
712785dc44 | ||
|
|
f904057944 | ||
|
|
4cc750ec0f | ||
|
|
f97f43f49b | ||
|
|
826e42085c | ||
|
|
4e555f3198 | ||
|
|
df5d23abbc | ||
|
|
8041f6b268 | ||
|
|
f22e2d6d93 | ||
|
|
d770b218f2 | ||
|
|
93d640206f | ||
|
|
19d66a3cb2 | ||
|
|
9819668246 | ||
|
|
944a29814b | ||
|
|
b09ef174ab | ||
|
|
aebf33c2f0 | ||
|
|
5e59b7b561 | ||
|
|
f47efb8f5f | ||
|
|
c562b9525a | ||
|
|
06593afb95 | ||
|
|
9d09869f80 | ||
|
|
32ecbfc718 | ||
|
|
ae6fdeef29 | ||
|
|
bbd0cdee78 | ||
|
|
76ab984e0b | ||
|
|
c61c9c86c8 | ||
|
|
90e29f8b43 | ||
|
|
e7ab1e4296 | ||
|
|
e5017a2031 | ||
|
|
02ff35a306 | ||
|
|
c3b90d7952 | ||
|
|
f9ba58c6f1 | ||
|
|
69da2dbd18 | ||
|
|
308b4a9ecd | ||
|
|
1256f02c13 | ||
|
|
b3ee050eb3 | ||
|
|
ead26a92c4 | ||
|
|
2e4dca4d89 | ||
|
|
b4d1e3eaff | ||
|
|
8a5fa4f158 | ||
|
|
fcacca3495 | ||
|
|
6e71478598 | ||
|
|
e982a8a799 | ||
|
|
b591947fac | ||
|
|
6ed171fc8f | ||
|
|
48f68f82dd | ||
|
|
70fbc0c24a | ||
|
|
27049809ef | ||
|
|
5b4ff8b5c2 | ||
|
|
0f581ad9cb | ||
|
|
edf9163ca1 | ||
|
|
83b6e82c1d | ||
|
|
c0e54d44c4 | ||
|
|
03f20e7ec7 | ||
|
|
c1e9d01099 | ||
|
|
0f4f2674f7 | ||
|
|
6e752bba1c | ||
|
|
077d6ba8a4 | ||
|
|
0e3b9d9c1d | ||
|
|
21edb60c16 | ||
|
|
b38cd59b42 | ||
|
|
fbf88be324 | ||
|
|
cfa228083d | ||
|
|
b9b36e31d2 | ||
|
|
f25259e78b | ||
|
|
7e24125fe3 | ||
|
|
e237f76b0f | ||
|
|
a5c9a8cea6 | ||
|
|
7dca17f2aa | ||
|
|
2befc5c172 | ||
|
|
8a667cb8a2 | ||
|
|
fde1e96104 | ||
|
|
13ad09022b | ||
|
|
7c248803dd | ||
|
|
b4b440f871 | ||
|
|
86c29a89c2 | ||
|
|
ac3255efb5 | ||
|
|
24acd8e2fe | ||
|
|
a243d7a0ff | ||
|
|
530715af17 | ||
|
|
10e2c0c20f | ||
|
|
17b1de0825 | ||
|
|
882478edb1 | ||
|
|
a5e3343f06 | ||
|
|
168e4427dc | ||
|
|
069a44774e | ||
|
|
e67d60d088 | ||
|
|
d1239e2e22 | ||
|
|
3e1ee77c14 | ||
|
|
973f1386d0 | ||
|
|
077ed36a4a | ||
|
|
1841ee32f6 | ||
|
|
d16a1938c2 | ||
|
|
28f3c3be62 | ||
|
|
1d2e0fed0e | ||
|
|
065048ee46 | ||
|
|
465a982080 | ||
|
|
8827fae5f3 | ||
|
|
3461a2d36c | ||
|
|
19891a39e4 | ||
|
|
b751959f42 | ||
|
|
721a599ed3 | ||
|
|
9caf10774b | ||
|
|
b1dd37025f | ||
|
|
9b49a321bb | ||
|
|
f8b0214519 | ||
|
|
4bcc9dbc3a | ||
|
|
4b44f3c718 | ||
|
|
230ce5e405 | ||
|
|
ca2bc58881 | ||
|
|
8271ccb005 | ||
|
|
e766a9a581 | ||
|
|
b66a0a9b12 | ||
|
|
fb7733f2e1 | ||
|
|
d4fe8943a4 | ||
|
|
c3d6e845d0 | ||
|
|
ba7b0843df | ||
|
|
5bbd6d474a | ||
|
|
e3d1d2760a | ||
|
|
4489e171db | ||
|
|
9c32bf2328 | ||
|
|
1c687f84b7 | ||
|
|
a574687253 | ||
|
|
b7464a3742 | ||
|
|
f319d2f498 | ||
|
|
61bb42ae3a | ||
|
|
e5bd0dfe49 | ||
|
|
f30adeb613 | ||
|
|
ca173694f8 | ||
|
|
9b04008545 | ||
|
|
83ca847312 | ||
|
|
19dcb55980 | ||
|
|
f1f1e19670 | ||
|
|
1bb23e2198 | ||
|
|
adfd7baa46 | ||
|
|
c0b33e1ea5 | ||
|
|
066bb84e5c | ||
|
|
e230ee77fd | ||
|
|
e113373eb6 | ||
|
|
dd602cd3b6 | ||
|
|
ce029693cf | ||
|
|
497af1fe00 | ||
|
|
b5e5bc0b01 | ||
|
|
051b1875e7 | ||
|
|
a673f4a7f2 | ||
|
|
bd1804589a | ||
|
|
26409139a0 | ||
|
|
62e44488a9 | ||
|
|
5def82024d | ||
|
|
cdb6b6d8f8 | ||
|
|
4705c3ec8c | ||
|
|
ded547a296 | ||
|
|
c61655bb08 | ||
|
|
eb25eeef76 | ||
|
|
1de015a3ab | ||
|
|
b83dbbe5d4 | ||
|
|
ed82534030 | ||
|
|
94481421ac | ||
|
|
3463563e42 | ||
|
|
b6fac871e0 | ||
|
|
6221193d8c | ||
|
|
83f14010b1 | ||
|
|
adb6f8327d | ||
|
|
bb7ede7998 | ||
|
|
b6103a61cd | ||
|
|
8ac4b2c7dd | ||
|
|
ed15907e69 | ||
|
|
022f358e16 | ||
|
|
661ce426dd | ||
|
|
068b89fe72 | ||
|
|
0924ebe33d | ||
|
|
97b7429da5 | ||
|
|
cc1c392418 | ||
|
|
1e8ce0bebf | ||
|
|
badcdf78c1 | ||
|
|
026bf60751 | ||
|
|
6aee85a591 | ||
|
|
f8e150c65d | ||
|
|
694d0ac27c | ||
|
|
2ad683f7dd | ||
|
|
da367ee93f | ||
|
|
190d2304bc | ||
|
|
bdc39b25b7 | ||
|
|
9b17434c64 | ||
|
|
65426df448 | ||
|
|
05977c7792 | ||
|
|
804e92e9b7 | ||
|
|
02420b5e94 | ||
|
|
b3f7016515 | ||
|
|
96c7166a26 | ||
|
|
4262a1b639 | ||
|
|
ad9316377d | ||
|
|
c282f9c2d4 | ||
|
|
52a8ce60d9 | ||
|
|
517a87438e | ||
|
|
cf4d03b772 | ||
|
|
8877be24b5 | ||
|
|
1bbe9e19de | ||
|
|
6728f7c796 | ||
|
|
5488f4ba3d | ||
|
|
026b4aa7cf | ||
|
|
ab21227bee | ||
|
|
de0afbabde | ||
|
|
ef033e477c | ||
|
|
35b2cbc490 | ||
|
|
ecc031422e | ||
|
|
7451223e09 | ||
|
|
9a1ae415ce | ||
|
|
e09fe00563 | ||
|
|
0d8ae55422 | ||
|
|
ee508a7927 | ||
|
|
ec8b282767 | ||
|
|
face17249d | ||
|
|
ce182c9fa5 | ||
|
|
b25a59e748 | ||
|
|
3b0c50329a | ||
|
|
420245e66a | ||
|
|
dd9db08431 | ||
|
|
f8122d5fb3 | ||
|
|
90975c31d2 | ||
|
|
8dd630b43b | ||
|
|
25a41858fd | ||
|
|
d20986949b | ||
|
|
bc60281379 | ||
|
|
84b3df80e2 | ||
|
|
8923315995 | ||
|
|
528b4584ac | ||
|
|
c14d2c3867 | ||
|
|
be3081de42 | ||
|
|
c46e0d9811 | ||
|
|
f00df1c17a | ||
|
|
684f8769a1 | ||
|
|
956888956e | ||
|
|
d8de1f86cf | ||
|
|
4be4b84622 | ||
|
|
ce1fbb5e05 | ||
|
|
c29a7cd693 | ||
|
|
b69361abf2 | ||
|
|
e7e50bee35 | ||
|
|
2cef319a32 | ||
|
|
9df7c18181 | ||
|
|
b4229070e6 | ||
|
|
c11632f8ef | ||
|
|
972cd5e645 | ||
|
|
87e606699d | ||
|
|
5da4dee1cb | ||
|
|
0d44809496 | ||
|
|
74e0108d76 | ||
|
|
243fe9c756 | ||
|
|
276dff3ae1 | ||
|
|
e1ed469ec9 | ||
|
|
d60ac945fc | ||
|
|
82d0f81498 | ||
|
|
f176c1f117 | ||
|
|
341f4c006f | ||
|
|
a86721f992 | ||
|
|
d50805da83 | ||
|
|
7532cec613 | ||
|
|
6ef4e44c88 | ||
|
|
c147acedda | ||
|
|
b60c4e24e6 | ||
|
|
b4d8c82e04 | ||
|
|
4c197400da | ||
|
|
b422c44603 | ||
|
|
8fd7a5ad8f | ||
|
|
5cfcf77c32 | ||
|
|
e15a40f7d9 | ||
|
|
62905aa073 | ||
|
|
b98d3f7be2 | ||
|
|
1808a0473c | ||
|
|
1d8d609307 | ||
|
|
c8f9645eae | ||
|
|
0b83f75e14 | ||
|
|
ee2199afce | ||
|
|
b7a3719815 | ||
|
|
2b7a5c31d4 | ||
|
|
015c72292f | ||
|
|
303619d060 | ||
|
|
219c8c0ccc | ||
|
|
d3158cc8a1 | ||
|
|
d4c69004cc | ||
|
|
c0cd027904 | ||
|
|
12675a1586 | ||
|
|
a51d89f559 | ||
|
|
2c4d41f8da | ||
|
|
6491fc3ddd | ||
|
|
a51189e6f6 | ||
|
|
3bdee559e6 | ||
|
|
de67e11704 | ||
|
|
180380339d | ||
|
|
1d7be34787 | ||
|
|
d8e3c1a390 | ||
|
|
c4bf136fbd | ||
|
|
dc567ea94f | ||
|
|
b4133dbb7e | ||
|
|
3c8f2855bf | ||
|
|
9583a2a9e9 | ||
|
|
37646b4f63 | ||
|
|
96adf59413 | ||
|
|
6fadc6f4f8 | ||
|
|
4a2be8898d | ||
|
|
8bfbc93cf8 | ||
|
|
cb139b11ca | ||
|
|
e1e3ccc599 | ||
|
|
00e7d0793a | ||
|
|
6a0e2528db | ||
|
|
c612810a2a | ||
|
|
21243fd7a0 | ||
|
|
ca1b010529 | ||
|
|
a2d7297382 | ||
|
|
6052103ebf | ||
|
|
383cbe953f | ||
|
|
75d20f7e0f | ||
|
|
9fe7644f0a | ||
|
|
863c41d550 | ||
|
|
5280f71e15 | ||
|
|
df7b0772c8 | ||
|
|
4ea63506a5 | ||
|
|
fae1253df6 | ||
|
|
d9e074762b | ||
|
|
096cd4f9b9 | ||
|
|
39f6c8959e | ||
|
|
3824bcb359 | ||
|
|
3b56ce55ed | ||
|
|
31b8c24b4b | ||
|
|
a0b97b51c9 | ||
|
|
f45925d34d | ||
|
|
90a34c1295 | ||
|
|
42717759b6 | ||
|
|
695ca73817 | ||
|
|
f21a95940e | ||
|
|
7fe21927c9 | ||
|
|
8338777318 | ||
|
|
e2c1b65e9e | ||
|
|
e4f397d593 | ||
|
|
c0893aa500 | ||
|
|
726d78d4f8 | ||
|
|
918e31483b | ||
|
|
54960a9a99 | ||
|
|
71feae1ee1 | ||
|
|
317bf35c9c | ||
|
|
73b8fa039f | ||
|
|
8e1412c014 | ||
|
|
8cc777013c | ||
|
|
750f98648d | ||
|
|
995f36075a | ||
|
|
74a56142f6 | ||
|
|
cd9fc3e5a3 | ||
|
|
c8001a615e | ||
|
|
7be3266543 | ||
|
|
d0f013c536 | ||
|
|
d8f36cd716 | ||
|
|
9718d42c13 | ||
|
|
ed46dca3da | ||
|
|
07f40b5c15 | ||
|
|
e004968328 | ||
|
|
4a71d6fa73 | ||
|
|
b29db2c76c | ||
|
|
a817e001db | ||
|
|
35341a9caf | ||
|
|
7e469d6bea | ||
|
|
c1a5999471 | ||
|
|
9a193be6f3 | ||
|
|
9464f46bf6 | ||
|
|
f24a12ef45 | ||
|
|
f36252ac87 | ||
|
|
2653bf49d0 | ||
|
|
781b51ea88 | ||
|
|
36147e7b2f | ||
|
|
8842a097ab | ||
|
|
2a1445c060 | ||
|
|
bd24a34f8f | ||
|
|
fcee9b2003 | ||
|
|
923a2db2d0 | ||
|
|
59c2b6fa0f | ||
|
|
9984c4453f | ||
|
|
b439c8db3b | ||
|
|
cb828d577e | ||
|
|
4b48841caa | ||
|
|
adf422687a | ||
|
|
3a99faa85c | ||
|
|
e245659e5f | ||
|
|
17d85512a2 | ||
|
|
727d967823 | ||
|
|
54a51d9d82 | ||
|
|
5a0289a4b6 | ||
|
|
790181a720 | ||
|
|
a440b8d3bc | ||
|
|
e64025fb30 | ||
|
|
904d1650cd | ||
|
|
4765d96cec | ||
|
|
6a85c9588f | ||
|
|
7a6157fd12 | ||
|
|
f8cf6f9fb6 | ||
|
|
2cd39eb5c9 | ||
|
|
f823d7527d | ||
|
|
70732b7f94 | ||
|
|
6bf9858c94 | ||
|
|
f46c9847ce | ||
|
|
7a6480a5a1 | ||
|
|
6e1b95ff59 | ||
|
|
faf39b0e9c | ||
|
|
bae93001f0 | ||
|
|
a55985b415 | ||
|
|
794242f42f | ||
|
|
16846cfa26 | ||
|
|
a59e55642b | ||
|
|
78b074499b | ||
|
|
83851ba366 | ||
|
|
740f684324 | ||
|
|
f5240ed605 | ||
|
|
1d32c0f2cd | ||
|
|
a0d251eb9a | ||
|
|
6ecda5b99b | ||
|
|
61d4c735b3 | ||
|
|
d083f4bdf9 | ||
|
|
3175fd6518 | ||
|
|
9586c67d53 | ||
|
|
8db93a4ee4 | ||
|
|
585f35546f | ||
|
|
4eaadab2bc | ||
|
|
7e3781a662 | ||
|
|
286ac988c7 | ||
|
|
deaa8a47be | ||
|
|
b1d0ae9fee | ||
|
|
d69a5bfcda | ||
|
|
b63dfb136f | ||
|
|
aa58d5fe75 | ||
|
|
aab1c3e85b | ||
|
|
cbd00ca192 | ||
|
|
3e4392d4ad | ||
|
|
f5d85b8594 | ||
|
|
b1966a2a85 | ||
|
|
0516ebaa57 | ||
|
|
679fa7546c | ||
|
|
e76891b280 | ||
|
|
4d5da851ab | ||
|
|
d8c848cfae | ||
|
|
2df4c024f4 | ||
|
|
24d4971abe | ||
|
|
cc32cc8257 | ||
|
|
bd52e9c72d | ||
|
|
40b62be293 | ||
|
|
3db52a6c41 | ||
|
|
43f7ebb225 | ||
|
|
bd070a8af9 | ||
|
|
8e379b14f1 | ||
|
|
2d0cb64ec3 | ||
|
|
6291f48263 | ||
|
|
f103bda6fe | ||
|
|
7ea54b6f03 | ||
|
|
f4d04f6691 | ||
|
|
a2efc65226 | ||
|
|
c5117c7898 | ||
|
|
86761c405c | ||
|
|
4fc419ba74 | ||
|
|
8686012302 | ||
|
|
90f3f4b56f | ||
|
|
19edef364f | ||
|
|
ce307ddc45 | ||
|
|
948de91102 | ||
|
|
ab751c3b27 | ||
|
|
d01df11a21 | ||
|
|
0e4c9efdbe | ||
|
|
80438e991c | ||
|
|
d0e224dbe1 | ||
|
|
53138e405e | ||
|
|
a3a1e0d621 | ||
|
|
92efaab611 | ||
|
|
a87156690b | ||
|
|
fa01192bd3 | ||
|
|
0c2d9900f3 | ||
|
|
ebd34d0d0a | ||
|
|
f3ea9bcb47 | ||
|
|
33c5f74bf3 | ||
|
|
babb1f3956 | ||
|
|
c0b48208e6 | ||
|
|
b00b830f21 | ||
|
|
a24c56b527 | ||
|
|
bd1f4b06bc | ||
|
|
c3cbeebf91 | ||
|
|
bba3f071ee | ||
|
|
eaf5bb245b | ||
|
|
52bc01dede | ||
|
|
1b3020d99a | ||
|
|
046f0ce79d | ||
|
|
79c40bc505 | ||
|
|
c822777ed3 | ||
|
|
29ebb5ad72 | ||
|
|
4cdf9d9b63 | ||
|
|
120f81e3f5 | ||
|
|
0c70621935 | ||
|
|
9efa70a0ee | ||
|
|
b5dd14758b | ||
|
|
1cd8881088 | ||
|
|
e0654d8e08 | ||
|
|
b30ffc19cd | ||
|
|
3f0b64775f | ||
|
|
02bfc295e6 | ||
|
|
fcf957db9c | ||
|
|
fe62c7fcd4 | ||
|
|
a15e87f23c | ||
|
|
4375c1ecd3 | ||
|
|
d8d8ea3bd0 | ||
|
|
613c888d47 | ||
|
|
4c665f3749 | ||
|
|
307fe595f0 | ||
|
|
341d4ebf24 | ||
|
|
3adde31a47 | ||
|
|
6acd19977c | ||
|
|
df9bddb85f | ||
|
|
3d6928fcdc | ||
|
|
f242090f8d | ||
|
|
dd071d4926 | ||
|
|
e38ccdee96 | ||
|
|
7da26f92bf | ||
|
|
14c04c8ce1 | ||
|
|
5e928270bb | ||
|
|
9661a938c5 | ||
|
|
6ca806f733 | ||
|
|
ebb8a1c60f | ||
|
|
01d8d8b34d | ||
|
|
17f280708e | ||
|
|
3a0e3b780a | ||
|
|
3e1ca23348 | ||
|
|
22317cd092 | ||
|
|
9410d282cc | ||
|
|
3e732ddbe6 | ||
|
|
e4a7c2713f | ||
|
|
32e40ed930 | ||
|
|
1406b3566a | ||
|
|
7022a5a992 | ||
|
|
ef98cec45a | ||
|
|
c0477f52f5 | ||
|
|
111a0dcdca | ||
|
|
0113623294 | ||
|
|
c73857425a | ||
|
|
a53374f94f | ||
|
|
d0f771a78e | ||
|
|
0682433113 | ||
|
|
9f84983680 | ||
|
|
025b1e4a61 | ||
|
|
5fbcf19389 | ||
|
|
0b4f4f915a | ||
|
|
9c7cce6d07 | ||
|
|
fd3ae64d9d | ||
|
|
ece4e9f3e1 | ||
|
|
fda982c7d1 | ||
|
|
a4acf8caa7 | ||
|
|
da08b93e25 | ||
|
|
1f8229ca5c | ||
|
|
4afdbb94cf | ||
|
|
d591ec05c0 | ||
|
|
9c971a0387 | ||
|
|
e78d935a54 | ||
|
|
1304f59bef | ||
|
|
7a1aa3fb2a | ||
|
|
712fd47880 | ||
|
|
802c5cf20d | ||
|
|
8d2036636f | ||
|
|
b0923098e4 | ||
|
|
9673871c48 | ||
|
|
d08d2ab786 | ||
|
|
47608f26d5 | ||
|
|
6dee385660 | ||
|
|
ec7d82d663 | ||
|
|
567b47f793 | ||
|
|
f2603c030d | ||
|
|
2d24ea0e43 | ||
|
|
7f70f51c34 | ||
|
|
a5c3424d17 | ||
|
|
9cb3d2fde6 | ||
|
|
8526ba4d04 | ||
|
|
ffcd58ac46 | ||
|
|
1d02f1362c | ||
|
|
278a636398 | ||
|
|
831ca7c3cb | ||
|
|
5604cf1356 | ||
|
|
b339b89c4b | ||
|
|
cca24c8cbd | ||
|
|
67cc30305b | ||
|
|
53102be531 | ||
|
|
6b85d2398c | ||
|
|
7fd5ee818a | ||
|
|
04f149c072 | ||
|
|
c3edfd8acf | ||
|
|
9a7fc7c65c | ||
|
|
cc131cbf60 | ||
|
|
2c4e415a66 | ||
|
|
9bc4c3a210 | ||
|
|
b9198e5b28 | ||
|
|
67a5c44d06 | ||
|
|
39485f692c | ||
|
|
6a7c2dd778 | ||
|
|
20750ebddc | ||
|
|
b8cbcb8619 | ||
|
|
37fb2b1fb3 | ||
|
|
6950394054 | ||
|
|
a0ee00bc2a | ||
|
|
dd58bdc545 | ||
|
|
67d46cda28 | ||
|
|
06e04e1333 | ||
|
|
e8b745384c |
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = false
|
||||
22
.env.example
22
.env.example
@@ -9,18 +9,34 @@
|
||||
# Application URL
|
||||
APP_URL=https://example.com
|
||||
|
||||
# Use HTTP protocol and redirect to HTTPS
|
||||
#APP_FORCE_HTTPS=true
|
||||
# If you are using HTTPS, feel free to uncomment this line to improve security
|
||||
#SESSION_SECURE_COOKIE=true
|
||||
|
||||
# Enter your proxy address here if freescout.net is not available from your server
|
||||
# (access to freescout.net is required to obtain official modules)
|
||||
#APP_PROXY=
|
||||
|
||||
# Custom headers to add to all outgoing emails.
|
||||
#APP_CUSTOM_MAIL_HEADERS="IsTransactional:True;X-Custom-Header:value"
|
||||
|
||||
# Uncomment if you have many folders and you are experiencing performance issues
|
||||
#APP_UPDATE_FOLDER_COUNTERS_IN_BACKGROUND=true
|
||||
|
||||
# Timezones: https://github.com/freescout-helpdesk/freescout/wiki/PHP-Timezones
|
||||
# Comment it to use default timezone from php.ini
|
||||
#APP_TIMEZONE=Europe/London
|
||||
|
||||
# Comma separated list of trusted proxies for proper IP detection in FreeScout.
|
||||
# To trust all proxies that connect to your server use single asterisk: *
|
||||
# To trust ALL proxies, including those that are in a chain of forwarding use double asterisk: **
|
||||
#APP_TRUSTED_PROXIES=192.168.1.1,192.168.1.2,192.168.1.3
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
# Maximum password length is 50 characters
|
||||
DB_PASSWORD=
|
||||
|
||||
# Run the following console command to generate the key: php artisan key:generate
|
||||
@@ -28,4 +44,4 @@ DB_PASSWORD=
|
||||
APP_KEY=
|
||||
|
||||
# Uncomment to see errors in your browser, don't forget to comment it back when debugging finished
|
||||
#APP_DEBUG=true
|
||||
#APP_DEBUG=true
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: General Help Request
|
||||
about: Create a general help request
|
||||
|
||||
---
|
||||
<!--
|
||||
* If you have some error the first thing you should do is to check "Manage » Logs » App Logs"! If there are any errors there - make sure to provide them in the issue.
|
||||
* Have you read FAQ? https://github.com/freescout-helpdesk/freescout/wiki/FAQ
|
||||
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
|
||||
* Before creating an issue try to disable all non-official modules and remove all custom code from your FreeScout and see if the issue still persists.
|
||||
* Feature requests for modules should be posted here: https://freescout.net/request-feature/
|
||||
* Docker questions should go here: https://github.com/tiredofit/docker-freescout/issues
|
||||
* Create separate issues for different topics - no need to mix all your problems into one issue.
|
||||
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
|
||||
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned (and it can't be undone).
|
||||
* Mentioning how much you've spent on modules is also the way to be banned.
|
||||
|
||||
Still here? Well clean this out and go ahead :)
|
||||
-->
|
||||
|
||||
PHP version:
|
||||
FreeScout version:
|
||||
Database: MySQL / PostgreSQL
|
||||
Are you using CloudFlare: Yes / No
|
||||
Are you using non-official modules: Yes / No
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Keep in mind that pull requests should be sent to the `master` branch! See https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide#github-workflow
|
||||
|
||||
Now you can delete this text and type the description of your pull request...
|
||||
20
.github/workflows/lint-php.yml
vendored
Normal file
20
.github/workflows/lint-php.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: PHP Code Sniffer
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Lint PHP
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
tools: phpcs
|
||||
|
||||
- name: Run check
|
||||
run: phpcs
|
||||
59
.github/workflows/test-pgsql.yml
vendored
Normal file
59
.github/workflows/test-pgsql.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Test App (PostgreSQL)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test App (PostgreSQL)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DB_CONNECTION: testing_pgsql
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_USER: freescout-test
|
||||
POSTGRES_PASSWORD: freescout-test
|
||||
POSTGRES_DB: freescout-test
|
||||
ports:
|
||||
- 5432:5432
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: pgsql, mbstring, xml, imap, zip, gd, curl, intl, json
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: composer install --ignore-platform-reqs --no-interaction
|
||||
|
||||
- name: Migrate and seed the database
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate --force -n --database=testing_pgsql
|
||||
php${{ matrix.php }} artisan db:seed --force -n --database=testing_pgsql
|
||||
env:
|
||||
DB_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
|
||||
- name: Run PHP tests
|
||||
run: php${{ matrix.php }} ./vendor/bin/phpunit
|
||||
env:
|
||||
DB_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
45
.github/workflows/test.yml
vendored
Normal file
45
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Test App (MySQL)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test App (MySQL)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: mysql, mbstring, xml, imap, zip, gd, curl, intl, json
|
||||
|
||||
- name: Start MySQL
|
||||
run: |
|
||||
sudo systemctl start mysql
|
||||
|
||||
- name: Setup database
|
||||
run: |
|
||||
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `freescout-test`;'
|
||||
mysql -uroot -proot -e "CREATE USER 'freescout-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'freescout-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`freescout-test\`.* TO 'freescout-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: composer install --ignore-platform-reqs --no-interaction
|
||||
|
||||
- name: Migrate and seed the database
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate --force -n --database=testing
|
||||
php${{ matrix.php }} artisan db:seed --force -n --database=testing
|
||||
|
||||
- name: Run PHP tests
|
||||
run: php${{ matrix.php }} ./vendor/bin/phpunit
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -14,7 +14,7 @@ Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
|
||||
app/Console/Commands/Test*
|
||||
/bootstrap/compiled.php
|
||||
composer.phar
|
||||
#composer.lock
|
||||
@@ -27,8 +27,10 @@ Thumbs.db
|
||||
/Modules
|
||||
/Modules/**/.git
|
||||
/public/modules
|
||||
/public/docs
|
||||
/storage/.ignore_locales
|
||||
/storage/.installed
|
||||
/tools
|
||||
.well-known
|
||||
/resources/lang/module.*
|
||||
/resources/lang/module.*
|
||||
.phpunit.result.cache
|
||||
@@ -5,5 +5,6 @@
|
||||
# On some hostings it does not work as "public/$1", but works as "/public/$1".
|
||||
# But we can't write it as "/public/$1" because in this case it does not work
|
||||
# when application is installed in subdirectory.
|
||||
RewriteCond %{REQUEST_URI} !/\.well\-known/?.*
|
||||
RewriteRule (.*) public/$1 [L]
|
||||
</IfModule>
|
||||
@@ -3,6 +3,8 @@ language: php
|
||||
php:
|
||||
- 7.0
|
||||
- 7.2
|
||||
- 7.4
|
||||
- 8.0
|
||||
|
||||
before_script:
|
||||
- cp .env.travis .env
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -522,7 +522,7 @@ contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
any implied license or other defences to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
78
README.md
78
README.md
@@ -5,69 +5,79 @@
|
||||
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||
<br/><br/>
|
||||
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://github.com/freescout-helpdesk/freescout#requirements)
|
||||
[](https://freescout-helpdesk.github.io/img/badges/travis.svg) [](http://hits.dwyl.io/freescout-helpdesk/freescout)
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://hits.seeyoufarm.com)
|
||||
|
||||
</div>
|
||||
|
||||
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP7 (Laravel 5.5 framework) – self hosted clone of HelpScout. Now you can enjoy free Zendesk & Help Scout without giving up privacy or locking yourself into a service you don't control.
|
||||
**FreeScout** is the super lightweight and powerful free open source help desk and shared inbox built with PHP (Laravel framework). Now you can enjoy free Zendesk & Help Scout without giving up privacy or locking yourself into a service you don't control. FreeScout has been developed from scratch and is not using any copyrighted Help Scout or Zendesk materials.
|
||||
|
||||
FreeScout has been developed from scratch and is not using any copyrighted Help Scout materials to comply with GitHub's [DMCA Policy](https://help.github.com/articles/dmca-takedown-policy/) (trademark, design, logos, icons, CSS, JS, HTML files are copyrighted; idea cannot be copyrighted).
|
||||
If you want to support the project feel free to **star this repository**. It helps to increase the visibility of the project and let people know that it is valuable. Thanks for your support!
|
||||
|
||||

|
||||
|
||||
## Table of Contents
|
||||
* [Features](#features)
|
||||
* [Demo](#demo)
|
||||
* [Modules](#modules)
|
||||
* [Features](#features)
|
||||
* [Mobile Apps](#mobile-apps)
|
||||
* [Requirements](#requirements)
|
||||
* [News & Updates](#news--updates)
|
||||
* [Installation](#installation)
|
||||
* [Cloud Hosted](#cloud-hosted)
|
||||
* [Modules](#modules)
|
||||
* [Tools & Integrations](#tools--integrations)
|
||||
* [News & Updates](#news--updates)
|
||||
* [Contributing](#contributing)
|
||||
* [Screenshots](#screenshots)
|
||||
|
||||
## Demo
|
||||
|
||||
**[Live Demo](https://demo.freescout.net)**
|
||||
|
||||
## Features
|
||||
|
||||

|
||||
|
||||
* No limitations on the number of users, tickets, mailboxes, etc.
|
||||
* 100% Mobile-friendly.
|
||||
* Multilingual (English, French, Italian, Portuguese).
|
||||
* Multilingual: English, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, German, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish, Turkish.
|
||||
* Seamless email integration.
|
||||
* Supports modern Microsoft Exchange authentication.
|
||||
* Fully supports screen readers (for visually impaired).
|
||||
* Built with strong focus on [security](https://freescout.net/security).
|
||||
* Web installer & updater.
|
||||
* Starred conversations.
|
||||
* Forwarding conversations.
|
||||
* Merging conversations.
|
||||
* Moving conversations between mailboxes.
|
||||
* Phone conversations.
|
||||
* Sending new conversations to multiple recipients at once.
|
||||
* Collision detection – notice is shown when two agents open the same conversation.
|
||||
* Push notifications.
|
||||
* Following a conversation.
|
||||
* Auto reply.
|
||||
* Notes.
|
||||
* Internal notes.
|
||||
* Automatic refreshing of the conversations list without the need to reload the page.
|
||||
* Pasting screenshots from the clipboard into the reply area.
|
||||
* Configuring notifications on a per user basis.
|
||||
* Open tracking.
|
||||
* Editing threads.
|
||||
* Search.
|
||||
* And more…
|
||||
|
||||
Need anything else? Suggest features [here](https://feedback.userreport.com/25a3cb5f-e4bd-4470-b6f3-79fcfaa8e90f/#ideas/popular).
|
||||
Need anything else? Suggest features [here](https://freescout.net/request-feature/).
|
||||
|
||||
## Demo
|
||||
## Mobile Apps
|
||||
|
||||
[Live demo](https://demo.freescout.net)
|
||||
Mobile apps support the same functionality and modules as the web version of your FreeScout installation. Both support agents and administrators can use mobile apps.
|
||||
|
||||
## Modules
|
||||
|
||||
[Available modules](https://freescout.net/modules/)
|
||||
<a href="https://freescout.net/android-app/" target="_blank" rel="nofollow"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a> <a href="https://freescout.net/ios-app/" target="_blank" rel="nofollow"><img alt="iOS App" src="https://freescout-helpdesk.github.io/img/apps/ios.png?v=1" width="200px" /></a>
|
||||
|
||||
## Requirements
|
||||
|
||||
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
|
||||
|
||||
* Nginx / Apache / IIS
|
||||
* PHP 7.0+
|
||||
* PHP 7.1 - 8.x
|
||||
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
|
||||
|
||||
## News & Updates
|
||||
|
||||
If you would like to be notified about new releases, news and updates you can subscribe [here](https://feedburner.google.com/fb/a/mailverify?uri=freescout)
|
||||
There is no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -78,13 +88,37 @@ Images & one-click installs:
|
||||
* [Docker Image](http://freescout.net/docker/)
|
||||
* [Softaculous](http://www.softaculous.com/apps/customersupport/FreeScout) (cPanel, Plesk, ISPmanager, H-Sphere, DirectAdmin, InterWorx)
|
||||
* [Fantastico](http://ff3.netenberg.com/visitors/scripts/freescout/view) (cPanel, DirectAdmin, ISP Manager, ISP Config)
|
||||
* [Cloudron](https://cloudron.io/store/net.freescout.cloudronapp.html)
|
||||
* [Ubuntu](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
|
||||
|
||||
## Cloud Hosted
|
||||
|
||||
[Cloud Hosted FreeScout](https://freescout.net/cloud-hosted/)
|
||||
|
||||
## Modules
|
||||
|
||||
* [Official Modules](https://freescout.net/modules/)
|
||||
* [Community Modules](https://freescout.net/community-modules/)
|
||||
|
||||
## Tools & Integrations
|
||||
|
||||
* [API](https://api-docs.freescout.net/)
|
||||
* [Migrate to FreeScout](http://freescout.net/migrate/) (from any help desk)
|
||||
* [Zapier](https://freescout.net/zapier/)
|
||||
* [Make](https://freescout.net/make-integration/) (Integromat)
|
||||
|
||||
## News & Updates
|
||||
|
||||
Don't miss news, updates and new modules!
|
||||
|
||||
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [Telegram](https://freescout.net/telegram/) | [RSS](https://freescout.net/feed/)
|
||||
|
||||
## Contributing
|
||||
|
||||
* [Support the project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/issues/288)
|
||||
* [Development Guide](https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide)
|
||||
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
|
||||
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
|
||||
* [Support project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/wiki#support-project)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Email at support@freescout.net
|
||||
@@ -64,7 +64,7 @@ class ActivityLog extends Activity
|
||||
case self::DESCRIPTION_EMAILS_SENDING_ERROR_ALERT:
|
||||
return __('Error sending alert');
|
||||
case self::DESCRIPTION_EMAILS_SENDING_WRONG_EMAIL:
|
||||
return __('Error sending email to the user who replied to notiication from wrong email');
|
||||
return __('Error sending email to the user who replied to notification from wrong email');
|
||||
case self::DESCRIPTION_EMAILS_FETCHING_ERROR:
|
||||
return __('Error fetching email');
|
||||
case self::DESCRIPTION_SYSTEM_ERROR:
|
||||
@@ -96,7 +96,7 @@ class ActivityLog extends Activity
|
||||
case self::NAME_APP_LOGS:
|
||||
return __('App Logs');
|
||||
default:
|
||||
return ucfirst($log_name);
|
||||
return ucwords(str_replace('_', ' ', $log_name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ class Attachment extends Model
|
||||
|
||||
const DIRECTORY = 'attachment';
|
||||
|
||||
CONST DISK = 'private';
|
||||
|
||||
CONST MIME_TYPE_MAX_LENGTH = 127;
|
||||
|
||||
// https://github.com/Webklex/laravel-imap/blob/master/src/IMAP/Attachment.php
|
||||
public static $types = [
|
||||
'message' => self::TYPE_MESSAGE,
|
||||
@@ -32,13 +36,8 @@ class Attachment extends Model
|
||||
'other' => self::TYPE_OTHER,
|
||||
];
|
||||
|
||||
/**
|
||||
* Files with such extensions are being renamed on upload.
|
||||
*/
|
||||
public static $restricted_extensions = [
|
||||
'php.*',
|
||||
'sh',
|
||||
'pl',
|
||||
public static $type_extensions = [
|
||||
self::TYPE_VIDEO => ['flv', 'mp4', 'm3u8', 'ts', '3gp', 'mov', 'avi', 'wmv']
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
@@ -60,19 +59,59 @@ class Attachment extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check extension.
|
||||
$extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', $extension)) {
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = $file_name.'_';
|
||||
// Sanitize mime type.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3048
|
||||
$mime_duplicate = strpos($mime_type, "application/vnd.openxmlformats", 1);
|
||||
if ($mime_duplicate) {
|
||||
$mime_type = substr($mime_type, $mime_duplicate);
|
||||
}
|
||||
$mime_type = substr($mime_type, 0, self::MIME_TYPE_MAX_LENGTH);
|
||||
|
||||
$orig_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = \Helper::sanitizeUploadedFileName($file_name, $uploaded_file, $content);
|
||||
|
||||
// Replace some symbols in file name.
|
||||
// Gmail can not load image if it contains spaces.
|
||||
$file_name = str_replace(' ', '-', $file_name);
|
||||
$file_name = preg_replace('/[ #\/]/', '_', $file_name);
|
||||
// Replace soft hyphens.
|
||||
$file_name = str_replace(html_entity_decode('­'), '_', $file_name);
|
||||
|
||||
if (!$file_name) {
|
||||
if (!$orig_extension) {
|
||||
preg_match("/.*\/([^\/]+)$/", $mime_type, $m);
|
||||
if (!empty($m[1])) {
|
||||
$orig_extension = $m[1];
|
||||
}
|
||||
}
|
||||
$file_name = uniqid();
|
||||
if ($orig_extension) {
|
||||
$file_name .= '.'.$orig_extension;
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2385
|
||||
// Fix for webklex/php-imap.
|
||||
if ($file_name == 'undefined' && $mime_type == 'message/rfc822') {
|
||||
$file_name = 'RFC822.eml';
|
||||
}
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/1412#issuecomment-1658881493
|
||||
if ($file_name == 'undefined' && $mime_type == 'text/calendar') {
|
||||
$file_name = 'calendar.ics';
|
||||
}
|
||||
|
||||
if (strlen($file_name) > 255) {
|
||||
$without_ext = pathinfo($file_name, PATHINFO_FILENAME);
|
||||
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
// 125 because file name may have unicode symbols.
|
||||
$file_name = \Helper::substrUnicode($without_ext, 0, 125-strlen($extension)-1);
|
||||
$file_name .= '.'.$extension;
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
$type = self::detectType($mime_type);
|
||||
$type = self::detectType($mime_type, $orig_extension);
|
||||
}
|
||||
|
||||
$attachment = new self();
|
||||
@@ -84,6 +123,20 @@ class Attachment extends Model
|
||||
$attachment->embedded = $embedded;
|
||||
$attachment->save();
|
||||
|
||||
$file_info = self::saveFileToDisk($attachment, $file_name, $content, $uploaded_file);
|
||||
|
||||
$attachment->file_dir = $file_info['file_dir'];
|
||||
$attachment->size = Storage::disk(self::DISK)->size($file_info['file_path']);
|
||||
$attachment->save();
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file to the disk and return file_dir.
|
||||
*/
|
||||
public static function saveFileToDisk($attachment, $file_name, $content, $uploaded_file)
|
||||
{
|
||||
// Save file from content or copy file.
|
||||
// We have to keep file name as is, so if file exists we create extra folder.
|
||||
// Examples: 1/2/3
|
||||
@@ -93,21 +146,26 @@ class Attachment extends Model
|
||||
do {
|
||||
$i++;
|
||||
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
|
||||
} while (Storage::exists($file_path));
|
||||
} while (Storage::disk(self::DISK)->exists($file_path));
|
||||
|
||||
$file_dir .= $i.DIRECTORY_SEPARATOR;
|
||||
|
||||
if ($uploaded_file) {
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name);
|
||||
} else {
|
||||
Storage::put($file_path, $content);
|
||||
try {
|
||||
if ($uploaded_file) {
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
|
||||
} else {
|
||||
Storage::disk(self::DISK)->put($file_path, $content);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '[Attachment::saveFileToDisk()]');
|
||||
}
|
||||
|
||||
$attachment->file_dir = $file_dir;
|
||||
$attachment->size = Storage::size($file_path);
|
||||
$attachment->save();
|
||||
\Helper::sanitizeUploadedFileData($file_path, \Helper::getPrivateStorage(), $content);
|
||||
|
||||
return $attachment;
|
||||
return [
|
||||
'file_dir' => $file_dir,
|
||||
'file_path' => $file_path,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,13 +207,18 @@ class Attachment extends Model
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function detectType($mime_type)
|
||||
public static function detectType($mime_type, $extension = '')
|
||||
{
|
||||
if (preg_match("/^text\//", $mime_type)) {
|
||||
return self::TYPE_TEXT;
|
||||
} elseif (preg_match("/^message\//", $mime_type)) {
|
||||
return self::TYPE_MESSAGE;
|
||||
} elseif (preg_match("/^application\//", $mime_type)) {
|
||||
// This is tricky mime type.
|
||||
// For .mp4 mime type can be application/octet-stream
|
||||
if (!empty($extension) && in_array(strtolower($extension), self::$type_extensions[self::TYPE_VIDEO])) {
|
||||
return self::TYPE_VIDEO;
|
||||
}
|
||||
return self::TYPE_APPLICATION;
|
||||
} elseif (preg_match("/^audio\//", $mime_type)) {
|
||||
return self::TYPE_AUDIO;
|
||||
@@ -171,7 +234,7 @@ class Attachment extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Conver type name to integer.
|
||||
* Convert type name to integer.
|
||||
*/
|
||||
public static function typeNameToInt($type_name)
|
||||
{
|
||||
@@ -189,7 +252,46 @@ class Attachment extends Model
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return Storage::url($this->getStorageFilePath());
|
||||
$file_url = Storage::url($this->getStorageFilePath());
|
||||
|
||||
// Fix percents.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3530
|
||||
$file_url = str_replace('%', '%25', $file_url);
|
||||
|
||||
return $file_url.'?id='.$this->id.'&token='.$this->getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashed security token for the attachment.
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
// \Hash::make() may contain . and / symbols which may cause problems.
|
||||
return md5(config('app.key').$this->id.$this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the current Attachment as download
|
||||
*/
|
||||
public function download($view = false)
|
||||
{
|
||||
$headers = [];
|
||||
// #533
|
||||
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
|
||||
if ($view) {
|
||||
$headers['Content-Disposition'] = '';
|
||||
}
|
||||
$file_name = $this->file_name;
|
||||
|
||||
if ($file_name == "RFC822"){
|
||||
$file_name = $file_name.'.eml';
|
||||
}
|
||||
|
||||
return $this->getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
|
||||
}
|
||||
|
||||
private function getDisk() {
|
||||
return Storage::disk(self::DISK);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,14 +304,32 @@ class Attachment extends Model
|
||||
return self::formatBytes($this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* attachment/...
|
||||
*/
|
||||
public function getStorageFilePath()
|
||||
{
|
||||
return self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||
}
|
||||
|
||||
public function getLocalFilePath()
|
||||
/**
|
||||
* /var/html/storage/app/attachment/...
|
||||
*/
|
||||
public function getLocalFilePath($full = true)
|
||||
{
|
||||
return Storage::path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
if ($full) {
|
||||
return $this->getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
} else {
|
||||
return DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the attachment file actually exists on the disk.
|
||||
*/
|
||||
public function fileExists()
|
||||
{
|
||||
return $this->getDisk()->exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
}
|
||||
|
||||
public static function formatBytes($size, $precision = 0)
|
||||
@@ -239,30 +359,95 @@ class Attachment extends Model
|
||||
$attachments = self::whereIn('id', $attachment_ids)->get();
|
||||
|
||||
// Delete from disk
|
||||
foreach ($attachments as $attachment) {
|
||||
Storage::delete($attachment->getStorageFilePath());
|
||||
}
|
||||
|
||||
// Delete from DB
|
||||
self::whereIn('id', $attachment_ids)->delete();
|
||||
self::deleteForever($attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dummy ID for attachment.
|
||||
* Not used for now.
|
||||
* Delete attachments by thread IDs.
|
||||
*/
|
||||
/*public static function genrateDummyId()
|
||||
public static function deleteByThreadIds($thread_ids)
|
||||
{
|
||||
if (!$this->id) {
|
||||
// Math.random should be unique because of its seeding algorithm.
|
||||
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
|
||||
// after the decimal.
|
||||
$hash = mt_rand() / mt_getrandmax();
|
||||
$hash = base_convert($hash, 10, 36);
|
||||
$id = substr($hash, 2, 9);
|
||||
} else {
|
||||
$id = substr(md5($this->id), 0, 9);
|
||||
if (!count($thread_ids)) {
|
||||
return;
|
||||
}
|
||||
return $id;
|
||||
}*/
|
||||
$attachments = self::whereIn('thread_id', $thread_ids)->get();
|
||||
|
||||
// Delete from disk
|
||||
self::deleteForever($attachments);
|
||||
}
|
||||
|
||||
public static function deleteForever($attachments)
|
||||
{
|
||||
// Delete from disk
|
||||
foreach ($attachments as $attachment) {
|
||||
$attachment->getDisk()->delete($attachment->getStorageFilePath());
|
||||
}
|
||||
|
||||
// Delete from DB
|
||||
self::whereIn('id', $attachments->pluck('id')->toArray())->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete attachments and update Thread & Conversation.
|
||||
*/
|
||||
public static function deleteAttachments($attachments)
|
||||
{
|
||||
if (!$attachments instanceof \Illuminate\Support\Collection) {
|
||||
$attachments = collect($attachments);
|
||||
}
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if ($attachment->thread_id && $attachment->thread
|
||||
&& count($attachment->thread->attachments) <= 1
|
||||
) {
|
||||
$attachment->thread->has_attachments = false;
|
||||
$attachment->thread->save();
|
||||
// Update conversation.
|
||||
$conversation = $attachment->thread->conversation;
|
||||
foreach ($conversation->threads as $thread) {
|
||||
if ($thread->has_attachments) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
$conversation->has_attachments = false;
|
||||
$conversation->save();
|
||||
}
|
||||
}
|
||||
Attachment::deleteForever($attachments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the attachment and it's file.
|
||||
*/
|
||||
public function duplicate($thread_id = null)
|
||||
{
|
||||
$new_attachment = $this->replicate();
|
||||
|
||||
$new_attachment->thread_id = $thread_id;
|
||||
|
||||
$new_attachment->save();
|
||||
|
||||
try {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$this->getLocalFilePath(), $this->file_name,
|
||||
null, null, true
|
||||
);
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
|
||||
|
||||
if (!empty($file_info['file_dir'])) {
|
||||
$new_attachment->file_dir = $file_info['file_dir'];
|
||||
$new_attachment->save();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e);
|
||||
}
|
||||
|
||||
return $new_attachment;
|
||||
}
|
||||
|
||||
public function getFileContents()
|
||||
{
|
||||
return $this->getDisk()->get($this->getStorageFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
63
app/Console/Commands/CheckRequirements.php
Normal file
63
app/Console/Commands/CheckRequirements.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckRequirements extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:check-requirements';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check console version of PHP';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// PHP extensions.
|
||||
$php_extensions = \Helper::checkRequiredExtensions();
|
||||
|
||||
$this->comment("PHP Version");
|
||||
$this->line(' '.str_pad(phpversion(), 30, '.'). ' '.(version_compare(phpversion(), config('installer.core.minPhpVersion'), '>=') ? '<fg=green>OK</>' : '<fg=red>NOT FOUND</>'), false);
|
||||
|
||||
$this->comment("PHP Extensions");
|
||||
$this->output($php_extensions);
|
||||
|
||||
// Functions.
|
||||
$functions = \Helper::checkRequiredFunctions();
|
||||
|
||||
$this->comment("Functions");
|
||||
$this->output($functions);
|
||||
$this->line('');
|
||||
}
|
||||
|
||||
public function output($items)
|
||||
{
|
||||
foreach ($items as $item => $status) {
|
||||
$this->line(' '.str_pad($item, 30, '.'). ' '.($status ? '<fg=green>OK</>' : '<fg=red>NOT FOUND</>'), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
app/Console/Commands/CleanNotificationsTable.php
Normal file
48
app/Console/Commands/CleanNotificationsTable.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanNotificationsTable extends Command
|
||||
{
|
||||
const PERIOD = '-6 months';
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:clean-notifications-table';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete old read records from notifications table.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
\DB::table('notifications')->where('created_at', '<', \Carbon\Carbon::now()->modify(self::PERIOD))
|
||||
->whereNotNull('read_at')
|
||||
->delete();
|
||||
|
||||
$this->info('['.date('Y-m-d H:i:s').'] Deleted old read notifications for: '.self::PERIOD);
|
||||
}
|
||||
}
|
||||
46
app/Console/Commands/CleanSendLog.php
Normal file
46
app/Console/Commands/CleanSendLog.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanSendLog extends Command
|
||||
{
|
||||
const PERIOD = '-6 months';
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:clean-send-log';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete old records from send log.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$logs = \App\SendLog::where('created_at', '<', \Carbon\Carbon::now()->modify(self::PERIOD))->delete();
|
||||
|
||||
$this->info('['.date('Y-m-d H:i:s').'] Deleted send logs: '.self::PERIOD);
|
||||
}
|
||||
}
|
||||
46
app/Console/Commands/CleanTmp.php
Normal file
46
app/Console/Commands/CleanTmp.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
// When processing attachments FreeScout may create files in /tmp folder.
|
||||
// So it's good to clean this folder periodically.
|
||||
class CleanTmp extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:clean-tmp';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Remove from system temp folder FreeScout files older than 1 week to avoid "No space left on device"';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
\Helper::shellExec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
|
||||
|
||||
$this->comment("Done");
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,19 @@ class ClearCache extends Command
|
||||
$this->call('config:clear');
|
||||
} else {
|
||||
$this->call('config:cache');
|
||||
// Laravel users `require` function to include config.php
|
||||
// If opcache is being used for few seconds config.php is being cached.
|
||||
if (function_exists('opcache_invalidate')) {
|
||||
opcache_invalidate(app()->getCachedConfigPath());
|
||||
}
|
||||
}
|
||||
// Regenerate vars to get new data from .env
|
||||
if (!$this->option('doNotGenerateVars')) {
|
||||
$this->call('freescout:generate-vars');
|
||||
}
|
||||
// This should not be done during installation.
|
||||
if (\Helper::isInstalled()) {
|
||||
\Helper::queueWorkerRestart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class CreateUser extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:create-user';
|
||||
protected $signature = 'freescout:create-user {--role=} {--firstName=} {--lastName=} {--email=} {--password=}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -46,40 +46,57 @@ class CreateUser extends Command
|
||||
).'.model'
|
||||
);
|
||||
$user = new $class();
|
||||
$fillables = ['role', 'first_name', 'last_name', 'email', 'password'];
|
||||
foreach ($fillables as $key => $fillable) {
|
||||
if ($fillable == 'password') {
|
||||
$user->$fillable = \Hash::make($this->secret(($key + 1).'/'.count($fillables)." User $fillable"));
|
||||
} elseif ($fillable == 'role') {
|
||||
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable (admin/user)", 'admin');
|
||||
if (!$user->$fillable) {
|
||||
$user->$fillable = 'admin';
|
||||
}
|
||||
|
||||
while (!in_array($user->$fillable, User::$roles)) {
|
||||
$this->error('Incorrect role');
|
||||
$user->$fillable = $this->ask('Please enter valid role');
|
||||
}
|
||||
$user->$fillable = array_flip(User::$roles)[$user->$fillable];
|
||||
} else {
|
||||
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable");
|
||||
|
||||
if ($fillable == 'email') {
|
||||
while (!filter_var($user->$fillable, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Incorrect email address');
|
||||
$user->$fillable = $this->ask('Please enter valid email address');
|
||||
}
|
||||
}
|
||||
$user->role = $this->option('role');
|
||||
if ($user->role ) {
|
||||
if (!in_array($user->role , User::$roles)) {
|
||||
$this->error('Invalid role');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$user->role = $this->ask('User role (admin/user)', 'admin');
|
||||
while (!in_array($user->role, User::$roles)) {
|
||||
$this->error('Invalid role');
|
||||
$user->role = $this->ask('Please enter valid role');
|
||||
}
|
||||
}
|
||||
$user->role = array_flip(User::$roles)[$user->role];
|
||||
|
||||
$user->first_name = $this->option('firstName') ? $this->option('firstName') : $this->ask('User first name');
|
||||
$user->last_name = $this->option('lastName') ? $this->option('lastName') : $this->ask('User last name');
|
||||
|
||||
$user->email = $this->option('email');
|
||||
if ($user->email) {
|
||||
if (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Invalid email address');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$user->email = $this->ask('User email address');
|
||||
while (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Incorrect email address');
|
||||
$user->email = $this->ask('Please enter valid email address');
|
||||
}
|
||||
}
|
||||
|
||||
$user->password = \Hash::make($this->option('password') ? $this->option('password') : $this->secret('User password'));
|
||||
|
||||
if ($this->confirm('Do you want to create the user?', true)) {
|
||||
if ($user->isAdmin()) {
|
||||
$user->invite_state = User::INVITE_STATE_ACTIVATED;
|
||||
}
|
||||
$user->save();
|
||||
$this->info("User created (id: {$user->id})");
|
||||
|
||||
try {
|
||||
$user->save();
|
||||
} catch (\Exception $e) {
|
||||
$this->line($e->getMessage());
|
||||
$this->error('User already exists.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('User created with id: '.$user->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ class GenerateVars extends Command
|
||||
{
|
||||
try {
|
||||
$params = [
|
||||
'locales' => config('app.locales'),
|
||||
'locales' => \Helper::getAllLocales(),
|
||||
];
|
||||
|
||||
//$filesystem = new Filesystem();
|
||||
@@ -55,16 +55,21 @@ class GenerateVars extends Command
|
||||
|
||||
//$filesystem->put($file_path, $content);
|
||||
// Save vars only if content changed
|
||||
if (\Storage::exists('js/vars.js')) {
|
||||
$old_content = \Storage::get('js/vars.js');
|
||||
if ($content != $old_content) {
|
||||
try {
|
||||
if (\Storage::exists('js/vars.js')) {
|
||||
$old_content = \Storage::get('js/vars.js');
|
||||
if ($content != $old_content) {
|
||||
\Storage::put('js/vars.js', $content);
|
||||
}
|
||||
} else {
|
||||
\Storage::put('js/vars.js', $content);
|
||||
}
|
||||
} else {
|
||||
\Storage::put('js/vars.js', $content);
|
||||
$this->info("Created: ".substr($file_path, strlen(base_path())+1));
|
||||
} catch (\Exception $e) {
|
||||
$msg = "Error occurred saving /storage/app/public/js/vars.js. ".\Helper::formatException($e);
|
||||
\Log::error($msg);
|
||||
$this->error($msg);
|
||||
}
|
||||
|
||||
$this->info("Created: ".substr($file_path, strlen(base_path())+1));
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
|
||||
61
app/Console/Commands/LogoutUsers.php
Normal file
61
app/Console/Commands/LogoutUsers.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class LogoutUsers extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:logout-users';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Logout all users';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
// Remove files from storage/frameworks/sessions
|
||||
$files = \File::files(storage_path('framework/sessions'));
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
try {
|
||||
$deleted = \File::delete($file->getPathname());
|
||||
if ($deleted) {
|
||||
$count++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->line('Deleted sessions: '.$count);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,38 +43,60 @@ class ModuleCheckLicenses extends Command
|
||||
|
||||
$this->info('Active modules found: '.count($modules));
|
||||
|
||||
$params = [
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
'data' => [],
|
||||
];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
$license = $module->getLicense();
|
||||
|
||||
|
||||
if (!$module->isOfficial() || !$license) {
|
||||
continue;
|
||||
}
|
||||
$params = [
|
||||
'license' => $license,
|
||||
'module_alias' => $module->getAlias(),
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
];
|
||||
$result = WpApi::checkLicense($params);
|
||||
if (!empty($result['status']) && $result['status'] != 'valid') {
|
||||
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.json_encode($result);
|
||||
|
||||
$this->error($module->getName().': '.$msg);
|
||||
$data['license'] = $license;
|
||||
$data['module_alias'] = $module->getAlias();
|
||||
|
||||
// Deactive module
|
||||
\App\Module::deactiveModule($module->getAlias(), true);
|
||||
$params['data'][] = $data;
|
||||
}
|
||||
|
||||
// Inform admin
|
||||
\Log::error($msg);
|
||||
activity()
|
||||
->withProperties([
|
||||
'error' => $msg,
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_SYSTEM)
|
||||
->log(\App\ActivityLog::DESCRIPTION_SYSTEM_ERROR);
|
||||
} else {
|
||||
$this->info($module->getName().': OK');
|
||||
$result = WpApi::checkLicenses($params);
|
||||
|
||||
if (!empty($result['statuses'])) {
|
||||
foreach ($modules as $module) {
|
||||
$module_alias = $module->getAlias();
|
||||
|
||||
foreach ($result['statuses'] as $result_module_alias => $status) {
|
||||
if ($result_module_alias != $module_alias) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($status) && $status != 'valid') {
|
||||
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.$status;
|
||||
|
||||
$this->error($module->getName().': '.$msg);
|
||||
|
||||
// Deactive module
|
||||
\App\Module::deactiveModule($module->getAlias(), true);
|
||||
|
||||
// Inform admin
|
||||
\Log::error($msg);
|
||||
activity()
|
||||
->withProperties([
|
||||
'error' => $msg,
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_SYSTEM)
|
||||
->log(\App\ActivityLog::DESCRIPTION_SYSTEM_ERROR);
|
||||
} else {
|
||||
$this->info($module->getName().': OK');
|
||||
}
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$this->info($module->getName().': Unknown status');
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Checking licenses finished');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class ModuleInstall extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Install module or all modules (if module_alias is empty)';
|
||||
protected $description = 'Install module or all modules (if module_alias is empty): run migrations and create a symlink';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
@@ -86,19 +86,53 @@ class ModuleInstall extends Command
|
||||
$this->call('freescout:clear-cache');
|
||||
}
|
||||
|
||||
// There is similar function in \App\Module.
|
||||
public function createModulePublicSymlink($module)
|
||||
{
|
||||
$from = public_path('modules').DIRECTORY_SEPARATOR.$module->alias;
|
||||
$to = $module->getExtraPath('Public');
|
||||
|
||||
if (file_exists($from)) {
|
||||
return $this->info('Public symlink already exists');
|
||||
}
|
||||
|
||||
// file_exists() may throw "open_basedir restriction in effect".
|
||||
try {
|
||||
symlink($to, $from);
|
||||
// If module's Public is symlink.
|
||||
if (is_link($to)) {
|
||||
@unlink($to);
|
||||
}
|
||||
|
||||
// Symlimk may exist but lead to the module folder in a wrong case.
|
||||
// So we need first try to remove it.
|
||||
if (!file_exists($from) || !is_link($from)) {
|
||||
if (is_dir($from)) {
|
||||
@rename($from, $from.'_'.date('YmdHis'));
|
||||
} else {
|
||||
@unlink($from);
|
||||
}
|
||||
}
|
||||
|
||||
if (file_exists($from)) {
|
||||
return $this->info('Public symlink already exists');
|
||||
}
|
||||
|
||||
// Check target.
|
||||
if (!file_exists($to)) {
|
||||
// Try to create Public folder.
|
||||
try {
|
||||
\File::makeDirectory($to, \Helper::DIR_PERMISSIONS);
|
||||
} catch (\Exception $e) {
|
||||
// If it's a broken symlink.
|
||||
if (is_link($to)) {
|
||||
@unlink($to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
symlink($to, $from);
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Error occured creating ['.$from.'] symlink: '.$e->getMessage());
|
||||
$this->error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
|
||||
}
|
||||
|
||||
$this->info('The ['.$from.'] symlink has been created');
|
||||
|
||||
@@ -21,6 +21,12 @@ class ModuleLaroute extends Command
|
||||
*/
|
||||
protected $description = 'Generate a laravel routes JS-file for a module or all modules (if module_alias is empty)';
|
||||
|
||||
public $routes;
|
||||
|
||||
public $config;
|
||||
|
||||
public $generator;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
|
||||
167
app/Console/Commands/ModuleUpdate.php
Normal file
167
app/Console/Commands/ModuleUpdate.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* php artisan freescout:module-install modulealias.
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ModuleUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:module-update {module_alias?}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update all modules or a single module (if module_alias is set)';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$install_all = false;
|
||||
$modules = [];
|
||||
|
||||
// We have to clear modules cache first to update modules cache
|
||||
\Artisan::call('cache:clear');
|
||||
|
||||
// Create a symlink for the module (or all modules)
|
||||
$module_alias = $this->argument('module_alias');
|
||||
|
||||
$modules_directory = \WpApi::getModules();
|
||||
if (\WpApi::$lastError) {
|
||||
$this->error(__('Error occurred').': '.$lastError['message'].' ('.$lastError['code'].')');
|
||||
return;
|
||||
}
|
||||
|
||||
$installed_modules = \Module::all();
|
||||
|
||||
$counter = 0;
|
||||
$found = false;
|
||||
foreach ($modules_directory as $dir_module) {
|
||||
// Update single module.
|
||||
if ($module_alias && $dir_module['alias'] != $module_alias) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = true;
|
||||
|
||||
// Detect if new version is available.
|
||||
foreach ($installed_modules as $module) {
|
||||
if ($module->getAlias() != $dir_module['alias'] || !$module->active()) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module->get('version'), '>')) {
|
||||
$update_result = \App\Module::updateModule($dir_module['alias']);
|
||||
|
||||
$this->info('['.$update_result['module_name'].' Module'.']');
|
||||
if ($update_result['status'] == 'success') {
|
||||
$this->line($update_result['msg_success']);
|
||||
} else {
|
||||
$msg = $update_result['msg'];
|
||||
if ($update_result['download_msg']) {
|
||||
$msg .= ' ('.$update_result['download_msg'].')';
|
||||
}
|
||||
$this->error('ERROR: '.$msg);
|
||||
}
|
||||
if (trim($update_result['output'])) {
|
||||
$this->line(preg_replace("#\n#", "\n> ", '> '.trim($update_result['output'])));
|
||||
}
|
||||
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through each installed module
|
||||
foreach ($installed_modules as $module) {
|
||||
// Skip if the module is an official one
|
||||
if (\App\Module::isOfficial($module->get('authorUrl'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the URL for the latest version of the module
|
||||
$latest_version_number_url = $module->get('latestVersionUrl');
|
||||
if (! $latest_version_number_url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new Guzzle HTTP client
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
try {
|
||||
// Send a GET request to the latest version URL
|
||||
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
|
||||
|
||||
// Get the latest version number from the response body
|
||||
$latest_version = trim((string) $response->getBody());
|
||||
|
||||
if (empty($latest_version)) {
|
||||
continue;
|
||||
} else {
|
||||
// Get the current version of the module
|
||||
$current_version = $module->get('version');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If there's an exception, skip to the next iteration
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the latest version is greater than the current version
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// Update the module
|
||||
$update_result = \App\Module::updateModule($module->getAlias());
|
||||
|
||||
// Print the module name and status
|
||||
$this->info('[' . $update_result['module_name'] . ' Module' . ']');
|
||||
if ($update_result['status'] == 'success') {
|
||||
// If the update was successful, print the success message
|
||||
$this->line($update_result['msg_success']);
|
||||
} else {
|
||||
// If the update failed, print the error message
|
||||
$msg = $update_result['msg'];
|
||||
if ($update_result['download_msg']) {
|
||||
$msg .= ' (' . $update_result['download_msg'] . ')';
|
||||
}
|
||||
$this->error('ERROR: ' . $msg);
|
||||
}
|
||||
// If there's any output from the update, print it
|
||||
if (trim($update_result['output'])) {
|
||||
$this->line(preg_replace("#\n#", "\n> ", '> ' . trim($update_result['output'])));
|
||||
}
|
||||
|
||||
// Increment the counter
|
||||
$counter ++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($module_alias && !$found) {
|
||||
$this->error('Module with the following alias not found: '.$module_alias);
|
||||
} elseif (!$counter) {
|
||||
$this->line('All modules are up-to-date');
|
||||
}
|
||||
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
}
|
||||
105
app/Console/Commands/ParseEml.php
Normal file
105
app/Console/Commands/ParseEml.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mailbox;
|
||||
use Webklex\PHPIMAP\IMAP;
|
||||
use Webklex\PHPIMAP\Message;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
|
||||
class ParseEml extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* --mailbox Any mailbox able to connect via IMAP to its mail server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:parse-eml {--mailbox=2}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Parse EML file';
|
||||
|
||||
/**
|
||||
* Current mailbox.
|
||||
*
|
||||
* Used to process emails sent to multiple mailboxes.
|
||||
*/
|
||||
public $mailbox;
|
||||
|
||||
/**
|
||||
* Used to process emails sent to multiple mailboxes.
|
||||
*/
|
||||
public $mailboxes;
|
||||
|
||||
public $extra_import = [];
|
||||
|
||||
/**
|
||||
* Page size when requesting emails from mail server.
|
||||
*/
|
||||
const PAGE_SIZE = 300;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$email = file_get_contents(storage_path('logs/email.eml'));
|
||||
|
||||
if (!str_contains($email, "\r\n")){
|
||||
$email = str_replace("\n", "\r\n", $email);
|
||||
}
|
||||
|
||||
$raw_header = substr($email, 0, strpos($email, "\r\n\r\n"));
|
||||
$raw_body = substr($email, strlen($raw_header)+8);
|
||||
|
||||
$mailbox = Mailbox::find($this->option('mailbox'));
|
||||
|
||||
\Config::set('app.new_fetching_library', 'true');
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->openFolder("INBOX");
|
||||
|
||||
$message = Message::make(/*$this->option('uid')*/null, null, $client, $raw_header, $raw_body, [/*0 => "\\Seen"*/], IMAP::ST_UID);
|
||||
|
||||
$this->line('Headers: ');
|
||||
$this->info($message->getHeader()->raw);
|
||||
$this->line('From: ');
|
||||
$this->info(json_encode($message->getFrom()[0] ?? [], JSON_UNESCAPED_UNICODE));
|
||||
$this->line('In-Reply-To: ');
|
||||
$this->info($message->getInReplyTo());
|
||||
$this->line('References: ');
|
||||
$this->info(json_encode(array_values(array_filter(preg_split('/[, <>]/', $message->getReferences() ?? ''))), JSON_UNESCAPED_UNICODE));
|
||||
$this->line('Subject: ');
|
||||
$this->info($message->getSubject());
|
||||
$this->line('Text Body: ');
|
||||
$this->info($message->getTextBody());
|
||||
$this->line('HTML Body: ');
|
||||
$this->info($message->getHTMLBody(false));
|
||||
|
||||
$attachments = $message->getAttachments();
|
||||
if (count($attachments)) {
|
||||
$this->line('Attachments: ');
|
||||
foreach ($attachments as $attachment) {
|
||||
$this->info($attachment->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
app/Console/Commands/SendMonitor.php
Normal file
66
app/Console/Commands/SendMonitor.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Job;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SendMonitor extends Command
|
||||
{
|
||||
const CHECK_PERIOD = 12 * 3600; // 12 hours
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:send-monitor';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check if queue:work is processing emails queue and show an alert in the web interface if needed';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Get SendReplyToCustomer jobs.
|
||||
$pending_jobs = \App\Job::where('queue', 'emails')
|
||||
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
|
||||
->where('available_at', '<', time() - self::CHECK_PERIOD)
|
||||
->exists();
|
||||
|
||||
// Check failed_jobs.
|
||||
// No need - it can be done via Manage > Alerts > Logs Monitoring
|
||||
// if (!$pending_jobs) {
|
||||
// $pending_jobs = \App\FailedJob::where('queue', 'emails')
|
||||
// ->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
|
||||
// ->where('created_at', '<', time() - self::CHECK_PERIOD)
|
||||
// ->exists();
|
||||
// }
|
||||
|
||||
if ($pending_jobs) {
|
||||
\Option::set('send_emails_problem', '1');
|
||||
$this->error('['.date('Y-m-d H:i:s').'] There are problems with emails queue processing');
|
||||
} else {
|
||||
\Option::remove('send_emails_problem');
|
||||
$this->info('['.date('Y-m-d H:i:s').'] Emails queue processing is working');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class Update extends Command
|
||||
\Updater::update();
|
||||
$this->call('freescout:after-app-update');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Error occured: '.$e->getMessage());
|
||||
$this->error('Error occurred: '.$e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->info('You have the latest version installed: '.config('app.version'));
|
||||
|
||||
@@ -10,6 +10,8 @@ use App\Option;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
const FETCH_MAX_EXECUTION_TIME = 30; // minutes
|
||||
|
||||
/**
|
||||
* The Artisan commands provided by your application.
|
||||
*
|
||||
@@ -22,6 +24,7 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
* If --no-interaction flag is set the script will not run 'queue:work' daemon.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
*
|
||||
@@ -29,8 +32,11 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// Keep in mind that this function is also called on clearing cache.
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3970
|
||||
if (!$this->isScheduleRun() && !\Helper::isRoute('system.cron')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove failed jobs
|
||||
$schedule->command('queue:flush')
|
||||
->weekly();
|
||||
@@ -43,17 +49,35 @@ class Kernel extends ConsoleKernel
|
||||
->everyMinute()
|
||||
->withoutOverlapping();
|
||||
|
||||
$schedule->command('freescout:send-monitor')
|
||||
// Every 10 minutes.
|
||||
->cron('*/10 * * * *')
|
||||
->withoutOverlapping();
|
||||
|
||||
$schedule->command('freescout:update-folder-counters')
|
||||
->hourly();
|
||||
|
||||
$schedule->command('freescout:module-check-licenses')
|
||||
->daily();
|
||||
$app_key = config('app.key');
|
||||
if ($app_key) {
|
||||
$crc = crc32($app_key);
|
||||
$schedule->command('freescout:module-check-licenses')
|
||||
->cron((int)($crc % 59).' '.(int)($crc % 23).' * * *');
|
||||
}
|
||||
|
||||
// Check if user finished viewing conversation.
|
||||
$schedule->command('freescout:check-conv-viewers')
|
||||
->everyMinute()
|
||||
->withoutOverlapping();
|
||||
|
||||
$schedule->command('freescout:clean-send-log')
|
||||
->monthly();
|
||||
|
||||
$schedule->command('freescout:clean-notifications-table')
|
||||
->weekly();
|
||||
|
||||
$schedule->command('freescout:clean-tmp')
|
||||
->daily();
|
||||
|
||||
// Logs monitoring.
|
||||
$alert_logs_period = config('app.alert_logs_period');
|
||||
if (config('app.alert_logs') && $alert_logs_period) {
|
||||
@@ -79,9 +103,57 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
}
|
||||
|
||||
$fetch_unseen = (int)config('app.fetch_unseen');
|
||||
$fetch_command_identifier = \Helper::getWorkerIdentifier('freescout:fetch-emails');
|
||||
$fetch_command_name = 'freescout:fetch-emails'
|
||||
. ' --identifier='.$fetch_command_identifier
|
||||
. ' --unseen='.$fetch_unseen;
|
||||
|
||||
// Kill fetch commands running for too long.
|
||||
// In shedule:run this code is executed every time $schedule->command() in this function is executed.
|
||||
if (function_exists('shell_exec')) {
|
||||
$fetch_command_pids = \Helper::getRunningProcesses($fetch_command_identifier);
|
||||
|
||||
// The name of the command here must be exactly the same as below!
|
||||
// Otherwise long fetching will be killed and won't run longer than 1 mintue.
|
||||
// $schedule->command() creates a new mutex, so we need to store mutex name in cache.
|
||||
// $mutex_name = $schedule->command($fetch_command_name)
|
||||
// ->everyMinute() - this also need to be set
|
||||
// ->skip(function () {
|
||||
// return true;
|
||||
// })
|
||||
// ->mutexName();
|
||||
|
||||
$mutex_name = \Cache::get('fetch_mutex_name') ?? '';
|
||||
|
||||
// If there is no cache mutext but there are running fetch commands
|
||||
// it means the mutex had expired after self::FETCH_MAX_EXECUTION_TIME
|
||||
// and the existing command(s) is running longer than self::FETCH_MAX_EXECUTION_TIME.
|
||||
if (count($fetch_command_pids) > 0 && !\Cache::get($mutex_name)) {
|
||||
// Kill freescout:fetch-emails commands running for too long
|
||||
shell_exec('kill '.implode(' | kill ', $fetch_command_pids));
|
||||
} elseif (count($fetch_command_pids) == 0) {
|
||||
// Make sure 'ps' command actually works.
|
||||
$ps_works = \Helper::getRunningProcesses('schedule:run');
|
||||
|
||||
if (count($ps_works)) {
|
||||
// Previous freescout:fetch-emails may have been killed or errored and did not remove the mutex.
|
||||
// So here we are forcefully removing the mutex. Otherwise mutex will live for 24 hours.
|
||||
if (\Cache::has($mutex_name)) {
|
||||
\Cache::forget($mutex_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch emails from mailboxes
|
||||
$fetch_command = $schedule->command('freescout:fetch-emails')
|
||||
->withoutOverlapping()
|
||||
$fetch_command = $schedule->command($fetch_command_name)
|
||||
// withoutOverlapping() option creates a mutex in the cache
|
||||
// which by default expires in 24 hours.
|
||||
// So we are passing an 'expiresAt' parameter to withoutOverlapping() to
|
||||
// prevent fetching from not being executed when fetching command by some reason
|
||||
// does not remove the mutex from the cache.
|
||||
->withoutOverlapping($expiresAt = self::FETCH_MAX_EXECUTION_TIME /* minutes */)
|
||||
->sendOutputTo(storage_path().'/logs/fetch-emails.log');
|
||||
|
||||
switch (config('app.fetch_schedule')) {
|
||||
@@ -104,25 +176,43 @@ class Kernel extends ConsoleKernel
|
||||
$fetch_command->everyMinute();
|
||||
break;
|
||||
}
|
||||
\Cache::put('fetch_mutex_name', $fetch_command->mutexName(), self::FETCH_MAX_EXECUTION_TIME);
|
||||
|
||||
$schedule = \Eventy::filter('schedule', $schedule);
|
||||
|
||||
// If --no-daemonize flag is passed - do not run 'queue:work' daemon.
|
||||
foreach ($_SERVER['argv'] ?? [] as $arg) {
|
||||
if ($arg == '--no-interaction') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Command runs as subprocess and sets cache mutex. If schedule:run command is killed
|
||||
// subprocess does not clear the mutex and it stays in the cache until cache:clear is executed.
|
||||
// By default, the lock will expire after 24 hours.
|
||||
|
||||
$queue_work_params = Config('app.queue_work_params');
|
||||
// Add identifier to avoid conflicts with other FreeScout instances on the same server.
|
||||
$queue_work_params['--queue'] .= ','.\Helper::getWorkerIdentifier();
|
||||
|
||||
// $schedule->command('queue:work') command below has withoutOverlapping() option,
|
||||
// which works via special mutex stored in the cache preventing several 'queue:work' to work at the same time.
|
||||
// So when the cache is cleared the mutex indicating that the 'queue:work' is running is removed,
|
||||
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
|
||||
// that there are two 'queue:work' processes running and kills them.
|
||||
// After one minute 'queue:work' is executed by cron via `artisan schedule:run` and works in the background.
|
||||
if (function_exists('shell_exec')) {
|
||||
$running_commands = $this->getRunningQueueProcesses();
|
||||
$running_commands = \Helper::getRunningProcesses();
|
||||
|
||||
if (count($running_commands) > 1) {
|
||||
// Stop all queue:work processes.
|
||||
// queue:work command is stopped by settings a cache key
|
||||
\Helper::queueWorkRestart();
|
||||
\Helper::queueWorkerRestart();
|
||||
// Sometimes processes stuck and just continue running, so we need to kill them.
|
||||
// Sleep to let processes stop.
|
||||
sleep(1);
|
||||
// Check processes again.
|
||||
$worker_pids = $this->getRunningQueueProcesses();
|
||||
$worker_pids = \Helper::getRunningProcesses();
|
||||
|
||||
if (count($worker_pids) > 1) {
|
||||
// Current process also has to be killed, as otherwise it "stucks"
|
||||
@@ -135,36 +225,42 @@ class Kernel extends ConsoleKernel
|
||||
// }
|
||||
shell_exec('kill '.implode(' | kill ', $worker_pids));
|
||||
}
|
||||
} elseif (count($running_commands) == 0) {
|
||||
// Make sure 'ps' command actually works.
|
||||
$ps_works = \Helper::getRunningProcesses('schedule:run');
|
||||
|
||||
if (count($ps_works)) {
|
||||
// Previous queue:work may have been killed or errored and did not remove the mutex.
|
||||
// So here we are forcefully removing the mutex.
|
||||
$mutex_name = $schedule->command('queue:work', $queue_work_params)
|
||||
->skip(function () {
|
||||
return true;
|
||||
})
|
||||
->mutexName();
|
||||
if (\Cache::get($mutex_name)) {
|
||||
\Cache::forget($mutex_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$schedule->command('queue:work', Config('app.queue_work_params'))
|
||||
$schedule->command('queue:work', $queue_work_params)
|
||||
->everyMinute()
|
||||
->withoutOverlapping()
|
||||
->sendOutputTo(storage_path().'/logs/queue-jobs.log');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pids of the queue:work processes.
|
||||
*
|
||||
* @return [type] [description]
|
||||
* This function is needed because every time $schedule->command() is executed
|
||||
* the schedule() is executed also.
|
||||
*/
|
||||
protected function getRunningQueueProcesses()
|
||||
public function isScheduleRun()
|
||||
{
|
||||
$pids = [];
|
||||
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep 'queue:work'"));
|
||||
foreach ($processes as $process) {
|
||||
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
|
||||
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
|
||||
$pids[] = $m[1];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing
|
||||
if (\Helper::isConsole()) {
|
||||
return true;
|
||||
} else {
|
||||
return !empty($_SERVER['argv']) && in_array('schedule:run', $_SERVER['argv']);
|
||||
}
|
||||
return $pids;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,6 +272,13 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
// Swiftmailer uses $_SERVER['SERVER_NAME'] in transport_deps.php
|
||||
// to set the host for EHLO command, if it is empty it uses [127.0.0.1].
|
||||
// G Suite sometimes rejects emails with EHLO [127.0.0.1].
|
||||
if (empty($_SERVER['SERVER_NAME'])) {
|
||||
$_SERVER['SERVER_NAME'] = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
|
||||
1378
app/Conversation.php
1378
app/Conversation.php
File diff suppressed because it is too large
Load Diff
757
app/Customer.php
757
app/Customer.php
File diff suppressed because it is too large
Load Diff
58
app/CustomerChannel.php
Normal file
58
app/CustomerChannel.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CustomerChannel extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'customer_channel';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Cached communication channels.
|
||||
*/
|
||||
public static $channels = null;
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo('App\Customer');
|
||||
}
|
||||
|
||||
public static function create($customer_id, $channel, $channel_id)
|
||||
{
|
||||
try {
|
||||
$customer_channel = new self();
|
||||
$customer_channel->customer_id = $customer_id;
|
||||
$customer_channel->channel = $channel;
|
||||
$customer_channel->channel_id = $channel_id;
|
||||
$customer_channel->save();
|
||||
|
||||
return $customer_channel;
|
||||
} catch (\Exception $e) {
|
||||
// Already exists.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getChannelName()
|
||||
{
|
||||
return \Eventy::filter('channel.name', '', $this->channel);
|
||||
}
|
||||
|
||||
public static function getChannels()
|
||||
{
|
||||
if (self::$channels !== null) {
|
||||
return self::$channels;
|
||||
} else {
|
||||
self::$channels = \Eventy::filter('channels.list', []);
|
||||
return self::$channels;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ use Watson\Rememberable\Rememberable;
|
||||
class Email extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
/**
|
||||
* Email types.
|
||||
*/
|
||||
@@ -50,11 +52,15 @@ class Email extends Model
|
||||
// Email validation is not recommended:
|
||||
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address/201378#201378
|
||||
// So we just check for @
|
||||
if (!preg_match('/@/', $email)) {
|
||||
if (!preg_match('/^.+@.+$/', $email ?? '')) {
|
||||
return false;
|
||||
}
|
||||
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
|
||||
$email = mb_strtolower($email, 'UTF-8');
|
||||
// Remove trailing dots.
|
||||
$email = preg_replace("/\.+$/", '', $email);
|
||||
// Remove dot before @
|
||||
$email = preg_replace("/\.+@/", '@', $email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
@@ -63,4 +69,19 @@ class Email extends Model
|
||||
{
|
||||
return explode('@', $this->email)[0];
|
||||
}
|
||||
|
||||
public static function create($email, $customer_id, $type = self::TYPE_WORK)
|
||||
{
|
||||
try {
|
||||
$email_obj = new Email();
|
||||
$email_obj->email = $email;
|
||||
$email_obj->type = array_key_exists($type, self::$types) ? $type : self::TYPE_WORK;
|
||||
$email_obj->customer_id = $customer_id;
|
||||
$email_obj->save();
|
||||
|
||||
return $email_obj;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
app/Events/RealtimeChat.php
Normal file
102
app/Events/RealtimeChat.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/**
|
||||
* Fefresh chats list when new thread created in mailbox.
|
||||
*/
|
||||
namespace App\Events;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeChat implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The notification data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broadcast channel name for the event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function channelName()
|
||||
{
|
||||
if (!empty($this->data['mailbox_id'])) {
|
||||
return 'chat.'.$this->data['mailbox_id'];
|
||||
} else {
|
||||
return 'chat.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper funciton.
|
||||
*/
|
||||
public static function dispatchSelf($mailbox_id)
|
||||
{
|
||||
if (!\Helper::isChatModeAvailable()) {
|
||||
return;
|
||||
}
|
||||
$notification_data = [
|
||||
'mailbox_id' => $mailbox_id
|
||||
];
|
||||
event(new \App\Events\RealtimeChat($notification_data));
|
||||
}
|
||||
|
||||
public static function processPayload($payload)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||
|
||||
// Check if user can listen to this event.
|
||||
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Chats are retrieved in the template.
|
||||
$template_data = [
|
||||
'mailbox' => $mailbox,
|
||||
];
|
||||
|
||||
$payload->chats_html = \View::make('mailboxes/partials/chat_list')->with($template_data)->render();
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
118
app/Events/RealtimeConvNewThread.php
Normal file
118
app/Events/RealtimeConvNewThread.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* New thread created in conversation.
|
||||
*/
|
||||
namespace App\Events;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\Thread;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeConvNewThread implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The notification data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broadcast channel name for the event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function channelName()
|
||||
{
|
||||
if (!empty($this->data['conversation_id'])) {
|
||||
return 'conv.'.$this->data['conversation_id'];
|
||||
} else {
|
||||
return 'conv.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper funciton.
|
||||
*/
|
||||
public static function dispatchSelf($thread)
|
||||
{
|
||||
if ($thread->state != Thread::STATE_PUBLISHED) {
|
||||
return;
|
||||
}
|
||||
$notification_data = [
|
||||
'thread_id' => $thread->id,
|
||||
'conversation_id' => $thread->conversation_id,
|
||||
// conversation is prefetched in ThreadObserver.
|
||||
'mailbox_id' => $thread->conversation->mailbox_id,
|
||||
//'user_id' => $thread->created_by_user_id,
|
||||
];
|
||||
event(new \App\Events\RealtimeConvNewThread($notification_data));
|
||||
}
|
||||
|
||||
public static function processPayload($payload)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||
|
||||
// Check if user can listen to this event.
|
||||
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$thread = Thread::find($payload->thread_id);
|
||||
if (!$thread) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
// Add thread html to the payload.
|
||||
$template_data = [
|
||||
'conversation' => $thread->conversation,
|
||||
'mailbox' => $thread->conversation->mailbox,
|
||||
'threads' => [$thread],
|
||||
];
|
||||
|
||||
$payload->thread_html = \View::make('conversations/partials/threads')->with($template_data)->render();
|
||||
$payload->conversation_user_id = $thread->conversation->user_id;
|
||||
$payload->conversation_status = $thread->conversation->status;
|
||||
$payload->conversation_status_class = Conversation::$status_classes[$thread->conversation->status];
|
||||
$payload->conversation_status_icon = Conversation::$status_icons[$thread->conversation->status];
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
110
app/Events/RealtimeMailboxNewThread.php
Normal file
110
app/Events/RealtimeMailboxNewThread.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* New thread created in mailbox.
|
||||
*/
|
||||
namespace App\Events;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\Folder;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RealtimeMailboxNewThread implements ShouldBroadcastNow
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* The notification data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to broadcast.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastWith()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broadcast channel name for the event.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function channelName()
|
||||
{
|
||||
if (!empty($this->data['mailbox_id'])) {
|
||||
return 'mailbox.'.$this->data['mailbox_id'];
|
||||
} else {
|
||||
return 'mailbox.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper funciton.
|
||||
*/
|
||||
public static function dispatchSelf($mailbox_id)
|
||||
{
|
||||
$notification_data = [
|
||||
'mailbox_id' => $mailbox_id
|
||||
];
|
||||
event(new \App\Events\RealtimeMailboxNewThread($notification_data));
|
||||
}
|
||||
|
||||
public static function processPayload($payload)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||
|
||||
// Check if user can listen to this event.
|
||||
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$folder = null;
|
||||
$foler_id = Conversation::getFolderParam();
|
||||
if ($foler_id) {
|
||||
$folder = Folder::find($foler_id);
|
||||
}
|
||||
// Just in case.
|
||||
if (!$folder) {
|
||||
$folder = new Folder();
|
||||
}
|
||||
$template_data = [
|
||||
'folders' => $mailbox->getAssesibleFolders(),
|
||||
'folder' => $folder,
|
||||
'mailbox' => $mailbox,
|
||||
];
|
||||
|
||||
$payload->folders_html = \View::make('mailboxes/partials/folders')->with($template_data)->render();
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use App\User;
|
||||
|
||||
class UserDeleted
|
||||
{
|
||||
public $user;
|
||||
public $by_user;
|
||||
public $deleted_user;
|
||||
|
||||
/**
|
||||
@@ -14,9 +14,11 @@ class UserDeleted
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user, User $deleted_user)
|
||||
public function __construct(User $deleted_user, User $by_user)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->by_user = $by_user;
|
||||
$this->deleted_user = $deleted_user;
|
||||
|
||||
\Eventy::action('user.deleted', $deleted_user, $by_user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,27 @@ class FailedJob extends Model
|
||||
* Automatically converted into Carbon dates.
|
||||
*/
|
||||
protected $dates = ['failed_at'];
|
||||
|
||||
public $payload_decoded = null;
|
||||
|
||||
public function getPayloadDecoded()
|
||||
{
|
||||
if ($this->payload_decoded !== null) {
|
||||
return $this->payload_decoded;
|
||||
}
|
||||
|
||||
$this->payload_decoded = json_decode($this->payload, true);
|
||||
|
||||
return $this->payload_decoded;
|
||||
}
|
||||
|
||||
public function getCommand()
|
||||
{
|
||||
return \App\Job::getPayloadCommand($this->getPayloadDecoded());
|
||||
}
|
||||
|
||||
public static function retry($job_id)
|
||||
{
|
||||
\Artisan::call('queue:retry', ['id' => $job_id]);
|
||||
}
|
||||
}
|
||||
|
||||
164
app/Folder.php
164
app/Folder.php
@@ -17,18 +17,18 @@ class Folder extends Model
|
||||
const TYPE_DRAFTS = 30;
|
||||
const TYPE_ASSIGNED = 40;
|
||||
const TYPE_CLOSED = 60;
|
||||
const TYPE_DELETED = 70;
|
||||
const TYPE_SPAM = 80;
|
||||
const TYPE_DELETED = 110;
|
||||
|
||||
public static $types = [
|
||||
self::TYPE_UNASSIGNED => 'Unassigned',
|
||||
self::TYPE_MINE => 'Mine',
|
||||
self::TYPE_STARRED => 'Starred',
|
||||
self::TYPE_DRAFTS => 'Drafts',
|
||||
self::TYPE_ASSIGNED => 'Assigned',
|
||||
self::TYPE_CLOSED => 'Closed',
|
||||
self::TYPE_SPAM => 'Spam',
|
||||
self::TYPE_DELETED => 'Deleted',
|
||||
self::TYPE_STARRED => 'Starred',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -68,8 +68,16 @@ class Folder extends Model
|
||||
self::TYPE_STARRED,
|
||||
];
|
||||
|
||||
// Counter mode.
|
||||
const COUNTER_ACTIVE = 1;
|
||||
const COUNTER_TOTAL = 2;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the mailbox to which folder belongs.
|
||||
*/
|
||||
@@ -115,13 +123,13 @@ class Folder extends Model
|
||||
case self::TYPE_STARRED:
|
||||
return __('Starred');
|
||||
default:
|
||||
return __(self::$types[$this->type]);
|
||||
return __(\Eventy::filter('folder.type_name', self::$types[$this->type] ?? '', $this));
|
||||
}
|
||||
}
|
||||
|
||||
public function getTypeIcon()
|
||||
{
|
||||
return self::$type_icons[$this->type];
|
||||
return \Eventy::filter('folder.type_icon', self::$type_icons[$this->type] ?? '', $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,7 +152,6 @@ class Folder extends Model
|
||||
case self::TYPE_STARRED:
|
||||
$order_by[] = ['status' => 'asc'];
|
||||
$order_by[] = ['last_reply_at' => 'desc'];
|
||||
//$order_by = [['conversation_folder.id' => 'desc']];
|
||||
break;
|
||||
|
||||
case self::TYPE_DRAFTS:
|
||||
@@ -153,6 +160,7 @@ class Folder extends Model
|
||||
|
||||
case self::TYPE_CLOSED:
|
||||
$order_by = [['closed_at' => 'desc']];
|
||||
break;
|
||||
|
||||
case self::TYPE_SPAM:
|
||||
$order_by = [['last_reply_at' => 'desc']];
|
||||
@@ -161,6 +169,30 @@ class Folder extends Model
|
||||
case self::TYPE_DELETED:
|
||||
$order_by = [['user_updated_at' => 'desc']];
|
||||
break;
|
||||
|
||||
default:
|
||||
$order_by = \Eventy::filter('folder.conversations_order_by', $order_by, $this->type);
|
||||
break;
|
||||
}
|
||||
|
||||
// Process columns sorting.
|
||||
$sorting = Conversation::getConvTableSorting();
|
||||
if ($sorting['sort_by'] == 'date') {
|
||||
if ($sorting['order'] != 'desc') {
|
||||
foreach ($order_by as $block_i => $block) {
|
||||
foreach ($block as $field => $order) {
|
||||
if ($field == 'status') {
|
||||
unset($order_by[$block_i][$field]);
|
||||
} else {
|
||||
if ($order == 'desc') {
|
||||
$order_by[$block_i][$field] = 'asc';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$order_by = [[$sorting['sort_by'] => $sorting['order']]];
|
||||
}
|
||||
|
||||
return $order_by;
|
||||
@@ -191,11 +223,23 @@ class Folder extends Model
|
||||
|
||||
public function updateCounters()
|
||||
{
|
||||
if (config('app.update_folder_counters_in_background')) {
|
||||
\App\Jobs\UpdateFolderCounters::dispatch($this);
|
||||
} else {
|
||||
$this->updateCountersNow();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateCountersNow()
|
||||
{
|
||||
if (\Eventy::filter('folder.update_counters', false, $this)) {
|
||||
return;
|
||||
}
|
||||
if ($this->type == self::TYPE_MINE && $this->user_id) {
|
||||
$this->active_count = Conversation::where('user_id', $this->user_id)
|
||||
->where('mailbox_id', $this->mailbox_id)
|
||||
->where('status', Conversation::STATUS_ACTIVE)
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
->where('status', Conversation::STATUS_ACTIVE)
|
||||
->count();
|
||||
$this->total_count = Conversation::where('user_id', $this->user_id)
|
||||
->where('mailbox_id', $this->mailbox_id)
|
||||
@@ -237,7 +281,14 @@ class Folder extends Model
|
||||
*/
|
||||
public function getCount($folders = [])
|
||||
{
|
||||
if ($this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
|
||||
$counter = \Eventy::filter('folder.counter', self::COUNTER_ACTIVE, $this, $folders);
|
||||
|
||||
$count = \Eventy::filter('folder.count', false, $this, $counter, $folders);
|
||||
if ($count !== false) {
|
||||
return $count;
|
||||
}
|
||||
|
||||
if ($counter == self::COUNTER_TOTAL || $this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
|
||||
return $this->total_count;
|
||||
} else {
|
||||
return $this->getActiveCount($folders);
|
||||
@@ -251,10 +302,14 @@ class Folder extends Model
|
||||
{
|
||||
$active_count = $this->active_count;
|
||||
if ($this->type == self::TYPE_ASSIGNED) {
|
||||
if ($folders) {
|
||||
$mine_folder = $folders->firstWhere('type', self::TYPE_MINE);
|
||||
} else {
|
||||
$mine_folder = $this->mailbox->folders()->where('type', self::TYPE_MINE)->first();
|
||||
$mine_folder = \Eventy::filter('folder.active_count_mine_folder', null, $this, $folders);
|
||||
|
||||
if (!$mine_folder) {
|
||||
if ($folders) {
|
||||
$mine_folder = $folders->firstWhere('type', self::TYPE_MINE);
|
||||
} elseif ($this->mailbox_id) {
|
||||
$mine_folder = $this->mailbox->folders()->where('type', self::TYPE_MINE)->first();
|
||||
}
|
||||
}
|
||||
|
||||
if ($mine_folder) {
|
||||
@@ -268,20 +323,27 @@ class Folder extends Model
|
||||
return $active_count;
|
||||
}
|
||||
|
||||
public function getConversationsQuery()
|
||||
/**
|
||||
* Query for waiting since.
|
||||
*/
|
||||
public function getWaitingSinceQuery()
|
||||
{
|
||||
$query = null;
|
||||
|
||||
if ($this->type == self::TYPE_MINE) {
|
||||
// Assigned to user.
|
||||
return Conversation::where('user_id', $this->user_id)
|
||||
$query = Conversation::where('user_id', $this->user_id)
|
||||
->where('mailbox_id', $this->mailbox_id);
|
||||
} elseif ($this->isIndirect()) {
|
||||
// Via intermediate table.
|
||||
return Conversation::join('conversation_folder', 'conversations.id', '=', 'conversation_folder.conversation_id')
|
||||
$query = Conversation::join('conversation_folder', 'conversations.id', '=', 'conversation_folder.conversation_id')
|
||||
->where('conversation_folder.folder_id', $this->id);
|
||||
} else {
|
||||
// All other conversations.
|
||||
return $this->conversations();
|
||||
$query = $this->conversations();
|
||||
}
|
||||
|
||||
return \Eventy::filter('folder.waiting_since_query', $query, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,7 +354,7 @@ class Folder extends Model
|
||||
public function getWaitingSince()
|
||||
{
|
||||
// Get oldest active conversation.
|
||||
$conversation = $this->getConversationsQuery()
|
||||
$conversation = $this->getWaitingSinceQuery()
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
->where('status', Conversation::STATUS_ACTIVE)
|
||||
->orderBy($this->getWaitingSinceField(), 'asc')
|
||||
@@ -321,4 +383,74 @@ class Folder extends Model
|
||||
return'last_reply_at';
|
||||
}
|
||||
}
|
||||
|
||||
public function url($mailbox_id)
|
||||
{
|
||||
return \Eventy::filter('folder.url', route('mailboxes.view.folder', ['id'=>$mailbox_id, 'folder_id'=>$this->id]), $mailbox_id, $this);
|
||||
}
|
||||
|
||||
public static function create($data, $unique_per_user = true, $save = true)
|
||||
{
|
||||
if (!isset($data['mailbox_id']) || !isset($data['type'])) {
|
||||
return null;
|
||||
}
|
||||
$folder = new Folder ();
|
||||
$folder->mailbox_id = $data['mailbox_id'];
|
||||
$folder->type = $data['type'];
|
||||
|
||||
if (!empty($data['user_id'])) {
|
||||
if ($unique_per_user) {
|
||||
$user_folder = Folder::where('mailbox_id', $data['mailbox_id'])
|
||||
->where('user_id', $data['user_id'])
|
||||
->where('type', $data['type'])
|
||||
->first();
|
||||
// User folder already exists.
|
||||
if ($user_folder) {
|
||||
return $user_folder;
|
||||
}
|
||||
}
|
||||
$folder->user_id = $data['user_id'];
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$folder->save();
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta value.
|
||||
*/
|
||||
public function getMeta($key, $default = null)
|
||||
{
|
||||
$metas = $this->meta;
|
||||
if (isset($metas[$key])) {
|
||||
return $metas[$key];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set meta value.
|
||||
*/
|
||||
public function setMeta($key, $value)
|
||||
{
|
||||
$metas = $this->meta;
|
||||
$metas[$key] = $value;
|
||||
$this->meta = $metas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset thread meta value.
|
||||
*/
|
||||
public function unsetMeta($key)
|
||||
{
|
||||
$metas = $this->meta;
|
||||
if (isset($metas[$key])) {
|
||||
unset($metas[$key]);
|
||||
$this->meta = $metas;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
app/Follower.php
Normal file
10
app/Follower.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Follower extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
@@ -25,7 +27,9 @@ class LoginController extends Controller
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
protected $redirectTo = '/home';
|
||||
|
||||
public $decayMinutes = 10; // minutes to lockout
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
@@ -36,4 +40,40 @@ class LoginController extends Controller
|
||||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a login request to the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$this->validateLogin($request);
|
||||
|
||||
// If the class is using the ThrottlesLogins trait, we can automatically throttle
|
||||
// the login attempts for this application. We'll key this by the username and
|
||||
// the IP address of the client making these requests into this application.
|
||||
if ($this->hasTooManyLoginAttempts($request)) {
|
||||
$this->fireLockoutEvent($request);
|
||||
|
||||
return $this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
$custom_errors = \Eventy::filter('login.custom_check', [], $request);
|
||||
if ($custom_errors) {
|
||||
throw ValidationException::withMessages($custom_errors);
|
||||
}
|
||||
|
||||
if ($this->attemptLogin($request)) {
|
||||
return $this->sendLoginResponse($request);
|
||||
}
|
||||
|
||||
// If the login attempt was unsuccessful we will increment the number of attempts
|
||||
// to login and redirect the user back to the login form. Of course, when this
|
||||
// user surpasses their maximum number of attempts they will get locked out.
|
||||
$this->incrementLoginAttempts($request);
|
||||
|
||||
return $this->sendFailedLoginResponse($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
@@ -36,4 +39,24 @@ class ResetPasswordController extends Controller
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the given user's password.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\CanResetPassword $user
|
||||
* @param string $password
|
||||
* @return void
|
||||
*/
|
||||
protected function resetPassword($user, $password)
|
||||
{
|
||||
$user->password = Hash::make($password);
|
||||
|
||||
$user->setRememberToken(Str::random(60));
|
||||
|
||||
$user->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
|
||||
//$this->guard()->login($user);
|
||||
}
|
||||
}
|
||||
|
||||
1745
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
1745
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@@ -67,12 +67,26 @@ class CustomersController extends Controller
|
||||
//'emails' => 'array|required_without:first_name',
|
||||
//'emails.1' => 'nullable|email|required_without:first_name',
|
||||
'emails.*' => 'nullable|email|distinct|required_without:first_name',
|
||||
'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif',
|
||||
]);
|
||||
$validator->setAttributeNames([
|
||||
//'emails.1' => __('Email'),
|
||||
'photo_url' => __('Photo'),
|
||||
'emails.*' => __('Email'),
|
||||
]);
|
||||
|
||||
// Photo
|
||||
$validator->after(function ($validator) use ($customer, $request) {
|
||||
if ($request->hasFile('photo_url')) {
|
||||
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath(), $request->file('photo_url')->getMimeType());
|
||||
|
||||
if ($path_url) {
|
||||
$customer->photo_url = $path_url;
|
||||
} else {
|
||||
$validator->errors()->add('photo_url', __('Error occurred processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('customers.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
@@ -117,7 +131,7 @@ class CustomersController extends Controller
|
||||
'a_end' => '</a></strong>',
|
||||
]).' ';
|
||||
|
||||
$new_emails_change_customer[] = $email->email;
|
||||
$new_emails_change_customer[] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,26 +142,37 @@ class CustomersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$customer->fill($request->all());
|
||||
// Websites
|
||||
if (!empty($request->websites)) {
|
||||
$customer->setWebsites($request->websites);
|
||||
$request_data = $request->all();
|
||||
|
||||
if (isset($request_data['photo_url'])) {
|
||||
unset($request_data['photo_url']);
|
||||
}
|
||||
|
||||
$customer->setData($request_data);
|
||||
// Websites
|
||||
// if (!empty($request->websites)) {
|
||||
// $customer->setWebsites($request->websites);
|
||||
// }
|
||||
$customer->save();
|
||||
|
||||
$customer->syncEmails($request->emails);
|
||||
|
||||
// Update customer_id in all conversations to the current customer
|
||||
// Update customer_id in all conversations added to the current customer.
|
||||
foreach ($new_emails_change_customer as $new_email) {
|
||||
$conversations_to_change_customer = Conversation::where('customer_email', $new_email)->get();
|
||||
if ($new_email->customer_id) {
|
||||
$conversations_to_change_customer = Conversation::where('customer_id', $new_email->customer_id)->get();
|
||||
} else {
|
||||
// This does not work for phone conversations.
|
||||
$conversations_to_change_customer = Conversation::where('customer_email', $new_email->email)->get();
|
||||
}
|
||||
foreach ($conversations_to_change_customer as $conversation) {
|
||||
// We have to pass user to create line item and let others know that customer has changed.
|
||||
// Conversation may be even in other mailbox where user does not have an access.
|
||||
$conversation->changeCustomer($new_email, $customer, auth()->user());
|
||||
$conversation->changeCustomer($new_email->email, $customer, auth()->user());
|
||||
}
|
||||
}
|
||||
|
||||
// Update customer in conversations for removed emails
|
||||
// Update customer in conversations for emails removed from current customer.
|
||||
foreach ($removed_emails as $removed_email) {
|
||||
$email = Email::where('email', $removed_email)->first();
|
||||
if ($email) {
|
||||
@@ -158,8 +183,12 @@ class CustomersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('customer.updated', $customer);
|
||||
|
||||
$flash_message = __('Customer saved successfully.').' '.$flash_message;
|
||||
\Session::flash('flash_success_unescaped', $flash_message);
|
||||
|
||||
\Session::flash('customer.updated', 1);
|
||||
|
||||
return redirect()->route('customers.update', ['id' => $id]);
|
||||
}
|
||||
@@ -188,7 +217,7 @@ class CustomersController extends Controller
|
||||
$user = User::findOrFail($id);
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$user->mailboxes()->sync($request->mailboxes);
|
||||
$user->mailboxes()->sync($request->mailboxes ?: []);
|
||||
|
||||
\Session::flash('flash_success', __('Permissions saved successfully'));
|
||||
|
||||
@@ -206,6 +235,7 @@ class CustomersController extends Controller
|
||||
|
||||
$conversations = $customer->conversations()
|
||||
->where('customer_id', $customer->id)
|
||||
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
|
||||
@@ -260,7 +290,11 @@ class CustomersController extends Controller
|
||||
->orWhere('last_name', 'like', '%'.$q.'%');
|
||||
}
|
||||
if ($request->search_by == 'phone') {
|
||||
$customers_query->where('customers.phones', 'like', '%'.$q.'%');
|
||||
$phone_numeric = \Helper::phoneToNumeric($q);
|
||||
if (!$phone_numeric) {
|
||||
$phone_numeric = $q;
|
||||
}
|
||||
$customers_query->where('customers.phones', 'like', '%'.$phone_numeric.'%');
|
||||
}
|
||||
|
||||
$customers = $customers_query->paginate(20);
|
||||
@@ -281,7 +315,8 @@ class CustomersController extends Controller
|
||||
// Get phone which matches
|
||||
$phones = $customer->getPhones();
|
||||
foreach ($phones as $phone) {
|
||||
if (strstr($phone['value'], $q)) {
|
||||
$phone_numeric = \Helper::phoneToNumeric($q);
|
||||
if (strstr($phone['value'], $q) || strstr($phone['n'] ?? '', $phone_numeric)) {
|
||||
$text = $phone['value'];
|
||||
if ($customer->getFullName()) {
|
||||
$text .= ' — '.$customer->getFullName();
|
||||
@@ -303,7 +338,8 @@ class CustomersController extends Controller
|
||||
if (!empty($request->use_id)) {
|
||||
$id = $customer->id;
|
||||
} else {
|
||||
$id = $customer->email;
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/4057
|
||||
$id = $customer->email ?? $customer->getMainEmail();
|
||||
}
|
||||
}
|
||||
$response['results'][] = [
|
||||
@@ -316,4 +352,69 @@ class CustomersController extends Controller
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax controller.
|
||||
*/
|
||||
public function ajax(Request $request)
|
||||
{
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'msg' => '', // this is error message
|
||||
];
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
switch ($request->action) {
|
||||
|
||||
// Change conversation user
|
||||
case 'create':
|
||||
// First name or email must be specified
|
||||
$validator = Validator::make($request->all(), [
|
||||
'first_name' => 'required|string|max:255',
|
||||
'last_name' => 'nullable|string|max:255',
|
||||
'email' => 'required|email|unique:emails,email',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->getMessages()as $errors) {
|
||||
foreach ($errors as $field => $message) {
|
||||
$response['msg'] .= $message.' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$customer = Customer::create($request->email, $request->all());
|
||||
if ($customer) {
|
||||
$response['email'] = $request->email;
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Conversations navigation
|
||||
case 'customers_pagination':
|
||||
|
||||
$customers = app('App\Http\Controllers\ConversationsController')->searchCustomers($request, $user);
|
||||
|
||||
$response['status'] = 'success';
|
||||
|
||||
$response['html'] = view('customers/partials/customers_table', [
|
||||
'customers' => $customers,
|
||||
])->render();
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occurred';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
}
|
||||
|
||||
570
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
570
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
@@ -29,9 +29,17 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function mailboxes()
|
||||
{
|
||||
//$this->authorize('create', 'App\Mailbox');
|
||||
//$mailboxes = Mailbox::all();
|
||||
$mailboxes = auth()->user()->mailboxesCanView();
|
||||
$user = auth()->user();
|
||||
|
||||
$mailboxes = $user->mailboxesCanView();
|
||||
|
||||
if (!\Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
|
||||
foreach ($mailboxes as $i => $mailbox) {
|
||||
if (!$user->canManageMailbox($mailbox->id)) {
|
||||
$mailboxes->forget($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('mailboxes/mailboxes', ['mailboxes' => $mailboxes]);
|
||||
}
|
||||
@@ -43,7 +51,7 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$this->authorize('create', 'App\Mailbox');
|
||||
|
||||
$users = User::where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
|
||||
return view('mailboxes/create', ['users' => $users]);
|
||||
}
|
||||
@@ -55,6 +63,8 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$this->authorize('create', 'App\Mailbox');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
@@ -64,7 +74,12 @@ class MailboxesController extends Controller
|
||||
|
||||
// //event(new Registered($user = $this->create($request->all())));
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (Mailbox::userEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('mailboxes.create')
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
@@ -72,9 +87,10 @@ class MailboxesController extends Controller
|
||||
|
||||
$mailbox = new Mailbox();
|
||||
$mailbox->fill($request->all());
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
$mailbox->users()->sync($request->users);
|
||||
$mailbox->users()->sync($request->users ?: []);
|
||||
$mailbox->syncPersonalFolders($request->users);
|
||||
|
||||
\Session::flash('flash_success_floating', __('Mailbox created successfully'));
|
||||
@@ -88,9 +104,29 @@ class MailboxesController extends Controller
|
||||
public function update($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
//$this->authorize('update', $mailbox);
|
||||
if (!auth()->user()->can('update', $mailbox)) {
|
||||
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
|
||||
$user = auth()->user();
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
$accessible_route = '';
|
||||
|
||||
$mailbox_settings = $user->mailboxSettings($mailbox->id);
|
||||
|
||||
if (!is_array($mailbox_settings->access)) {
|
||||
$access_permissions = json_decode($mailbox_settings->access ?? '');
|
||||
} else {
|
||||
$access_permissions = $mailbox_settings->access;
|
||||
}
|
||||
|
||||
if ($access_permissions && is_array($access_permissions)) {
|
||||
foreach ($access_permissions as $perm) {
|
||||
$accessible_route = Mailbox::getAccessPermissionRoute($perm);
|
||||
if ($accessible_route) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$accessible_route) {
|
||||
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
|
||||
}
|
||||
if ($accessible_route) {
|
||||
return redirect()->route($accessible_route, ['id' => $mailbox->id]);
|
||||
} else {
|
||||
@@ -98,9 +134,17 @@ class MailboxesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
if (!$mailbox_user && $user->isAdmin()) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$user->mailboxes()->attach($id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
}
|
||||
|
||||
//$mailboxes = Mailbox::all()->except($id);
|
||||
|
||||
return view('mailboxes/update', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
return view('mailboxes/update', ['mailbox' => $mailbox, 'mailbox_user' => $mailbox_user, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,31 +155,72 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function updateSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:40',
|
||||
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
|
||||
'aliases' => 'nullable|string|max:255',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
'template' => 'required|integer',
|
||||
'ticket_assignee' => 'required|integer',
|
||||
'signature' => 'nullable|string',
|
||||
]);
|
||||
|
||||
//event(new Registered($user = $this->create($request->all())));
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
|
||||
if ($user->can('updateSettings', $mailbox)) {
|
||||
|
||||
// Checkboxes
|
||||
$request->merge([
|
||||
'aliases_reply' => ($request->filled('aliases_reply') ?? false),
|
||||
]);
|
||||
|
||||
// if not admin, the text only fields don't pass so spike them into the request.
|
||||
if (!auth()->user()->isAdmin()) {
|
||||
$request->merge([
|
||||
'name' => $mailbox->name,
|
||||
'email' => $mailbox->email
|
||||
]);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:40',
|
||||
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
|
||||
'aliases' => 'nullable|string',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
'template' => 'required|integer',
|
||||
'ticket_assignee' => 'required|integer',
|
||||
]);
|
||||
|
||||
//event(new Registered($user = $this->create($request->all())));
|
||||
if (Mailbox::userEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
$validator = \Eventy::filter('mailbox.settings_validator', $validator, $mailbox, $request);
|
||||
|
||||
if ($invalid || count($validator->errors()) || $validator->fails()) {
|
||||
return redirect()->route('mailboxes.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->can('updateEmailSignature', $mailbox)) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'signature' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.email_signature', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('mailbox.settings_before_save', $mailbox, $request);
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
$mailbox->signature = \Helper::stripDangerousTags($mailbox->signature);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
@@ -151,11 +236,25 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updatePermissions', $mailbox);
|
||||
|
||||
$users = User::where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
return view('mailboxes/permissions', ['mailbox' => $mailbox, 'users' => $users, 'mailbox_users' => $mailbox->users]);
|
||||
$managers = User::nonDeleted()
|
||||
->select(['users.*', 'mailbox_user.hide', 'mailbox_user.access'])
|
||||
->leftJoin('mailbox_user', function ($join) use ($mailbox) {
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
$join->where('mailbox_user.mailbox_id', $mailbox->id);
|
||||
})->get();
|
||||
$managers = User::sortUsers($managers);
|
||||
|
||||
return view('mailboxes/permissions', [
|
||||
'mailbox' => $mailbox,
|
||||
'users' => $users,
|
||||
'managers' => $managers,
|
||||
'mailbox_users' => $mailbox->users,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,11 +266,47 @@ class MailboxesController extends Controller
|
||||
public function permissionsSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updatePermissions', $mailbox);
|
||||
|
||||
$mailbox->users()->sync($request->users);
|
||||
$user = auth()->user();
|
||||
|
||||
$mailbox->users()->sync(\Eventy::filter('mailbox.permission_users', $request->users, $id) ?: []);
|
||||
$mailbox->syncPersonalFolders($request->users);
|
||||
|
||||
// Save admins settings.
|
||||
$admins = User::nonDeleted()->where('role', User::ROLE_ADMIN)->get();
|
||||
foreach ($admins as $admin) {
|
||||
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
if (!$mailbox_user) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$admin->mailboxes()->attach($id);
|
||||
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
}
|
||||
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
|
||||
$mailbox_user->settings->save();
|
||||
}
|
||||
|
||||
// Sets the mailbox_user.access array
|
||||
$mailbox_users = $mailbox->users;
|
||||
foreach ($mailbox_users as $mailbox_user) {
|
||||
$access = [];
|
||||
$mailbox_with_settings = $mailbox_user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
|
||||
foreach (Mailbox::$access_permissions as $perm) {
|
||||
if (!empty($request->managers[$mailbox_user->id]['access'][$perm])) {
|
||||
$access[] = $request->managers[$mailbox_user->id]['access'][$perm];
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->id == $mailbox_user->id && !$user->isAdmin()) {
|
||||
// User with Permission priv's can't edit their own additional priv's.
|
||||
} else {
|
||||
$mailbox_with_settings->settings->access = json_encode($access);
|
||||
}
|
||||
$mailbox_with_settings->settings->hide = (isset($request->managers[$mailbox_user->id]['hide']) ? (int)$request->managers[$mailbox_user->id]['hide'] : false);
|
||||
$mailbox_with_settings->settings->save();
|
||||
}
|
||||
|
||||
\Session::flash('flash_success_floating', __('Mailbox permissions saved!'));
|
||||
|
||||
return redirect()->route('mailboxes.permissions', ['id' => $id]);
|
||||
@@ -183,7 +318,7 @@ class MailboxesController extends Controller
|
||||
public function connectionOutgoing($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
return view('mailboxes/connection', ['mailbox' => $mailbox, 'sendmail_path' => ini_get('sendmail_path'), 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
}
|
||||
@@ -194,7 +329,7 @@ class MailboxesController extends Controller
|
||||
public function connectionOutgoingSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
if ($request->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
@@ -212,7 +347,13 @@ class MailboxesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
// Do not save dummy password.
|
||||
if (preg_match("/^\*+$/", $request->out_password ?? '')) {
|
||||
$params = $request->except(['out_password']);
|
||||
} else {
|
||||
$params = $request->all();
|
||||
}
|
||||
$mailbox->fill($params);
|
||||
$mailbox->save();
|
||||
|
||||
if (!empty($request->send_test_to)) {
|
||||
@@ -220,8 +361,8 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
// Sometimes background job continues to use old connection settings.
|
||||
\Helper::queueWorkRestart();
|
||||
|
||||
\Helper::queueWorkerRestart();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Connection settings saved!'));
|
||||
|
||||
return redirect()->route('mailboxes.connection', ['id' => $id]);
|
||||
@@ -230,12 +371,26 @@ class MailboxesController extends Controller
|
||||
/**
|
||||
* Mailbox incoming settings.
|
||||
*/
|
||||
public function connectionIncoming($id)
|
||||
public function connectionIncoming($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
return view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
$fields = [
|
||||
'in_server' => $mailbox->in_server ?? '',
|
||||
'in_port' => $mailbox->in_port ?? '',
|
||||
'in_username' => $mailbox->in_username ?? '',
|
||||
'in_password' => $mailbox->in_password ?? '',
|
||||
];
|
||||
|
||||
$validator = Validator::make($fields, [
|
||||
'in_server' => 'required',
|
||||
'in_port' => 'required',
|
||||
'in_username' => 'required',
|
||||
'in_password' => 'required',
|
||||
]);
|
||||
|
||||
return view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)])->withErrors($validator);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,33 +399,42 @@ class MailboxesController extends Controller
|
||||
public function connectionIncomingSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'in_server' => 'required|string|max:255',
|
||||
'in_port' => 'required|integer',
|
||||
'in_username' => 'required|string|max:100',
|
||||
'in_password' => 'required|string|max:255',
|
||||
]);
|
||||
// $validator = Validator::make($request->all(), [
|
||||
// 'in_server' => 'nullable|string|max:255',
|
||||
// 'in_port' => 'nullable|integer',
|
||||
// 'in_username' => 'nullable|string|max:100',
|
||||
// 'in_password' => 'nullable|string|max:255',
|
||||
// ]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
// if ($validator->fails()) {
|
||||
// return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
|
||||
// ->withErrors($validator)
|
||||
// ->withInput();
|
||||
// }
|
||||
|
||||
// Checkboxes
|
||||
$request->merge([
|
||||
'in_validate_cert' => ($request->filled('in_validate_cert') ?? false),
|
||||
]);
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
// Do not save dummy password.
|
||||
if (preg_match("/^\*+$/", $request->in_password ?? '')) {
|
||||
$params = $request->except(['in_password']);
|
||||
} else {
|
||||
$params = $request->all();
|
||||
}
|
||||
|
||||
\Eventy::action('mailbox.incoming_settings_before_save', $mailbox, $request);
|
||||
|
||||
$mailbox->fill($params);
|
||||
|
||||
// Save IMAP Folders.
|
||||
// Save all custom folders except INBOX.
|
||||
$in_imap_folders = [];
|
||||
foreach ($request->in_imap_folders as $imap_folder) {
|
||||
if (mb_strtolower($imap_folder) != 'inbox') {
|
||||
if (is_array($request->in_imap_folders)) {
|
||||
foreach ($request->in_imap_folders as $imap_folder) {
|
||||
$in_imap_folders[] = $imap_folder;
|
||||
}
|
||||
}
|
||||
@@ -286,12 +450,12 @@ class MailboxesController extends Controller
|
||||
/**
|
||||
* View mailbox.
|
||||
*/
|
||||
public function view($id, $folder_id = null)
|
||||
public function view(Request $request, $id, $folder_id = null)
|
||||
{
|
||||
//auth()->guard()->attempt(['email' => '', 'password' => '']);
|
||||
$user = auth()->user();
|
||||
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('view', $mailbox);
|
||||
$mailbox = Mailbox::findOrFailWithSettings($id, $user->id);
|
||||
$this->authorize('viewCached', $mailbox);
|
||||
|
||||
$folders = $mailbox->getAssesibleFolders();
|
||||
|
||||
@@ -310,10 +474,10 @@ class MailboxesController extends Controller
|
||||
|
||||
$this->authorize('view', $folder);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
|
||||
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(
|
||||
Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->get('page')
|
||||
);
|
||||
|
||||
return view('mailboxes/view', [
|
||||
'mailbox' => $mailbox,
|
||||
@@ -327,7 +491,7 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$flashes = [];
|
||||
|
||||
if ($mailbox) {
|
||||
if ($mailbox && \Auth::user()->can('admin', $mailbox)) {
|
||||
if (Route::currentRouteName() != 'mailboxes.connection' && !$mailbox->isOutActive()) {
|
||||
$flashes[] = [
|
||||
'type' => 'warning',
|
||||
@@ -353,7 +517,7 @@ class MailboxesController extends Controller
|
||||
public function autoReply($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
if (!$mailbox->auto_reply_subject) {
|
||||
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||
@@ -371,7 +535,8 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
// $this->authorize('update', $mailbox);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
$request->merge([
|
||||
'auto_reply_enabled' => ($request->filled('auto_reply_enabled') ?? false),
|
||||
@@ -397,6 +562,7 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
$mailbox->auto_reply_message = \Helper::stripDangerousTags($mailbox->auto_reply_message);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
@@ -405,6 +571,24 @@ class MailboxesController extends Controller
|
||||
return redirect()->route('mailboxes.auto_reply', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto reply settings.
|
||||
*/
|
||||
public function emailSignature($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
if (!$mailbox->auto_reply_subject) {
|
||||
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||
}
|
||||
|
||||
return view('mailboxes/email_signature', [
|
||||
'mailbox' => $mailbox,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Users ajax controller.
|
||||
*/
|
||||
@@ -425,24 +609,37 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} elseif (empty($request->to)) {
|
||||
$response['msg'] = __('Please specify recipient of the test email');
|
||||
}
|
||||
|
||||
// Check if outgoing port is open.
|
||||
if (!$response['msg'] && $mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
$test_result = \Helper::checkPort($mailbox->out_server, $mailbox->out_port);
|
||||
if (!$test_result) {
|
||||
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->out_server.'</strong>', 'port' => '<strong>'.$mailbox->out_port.'</strong>']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$test_result = false;
|
||||
$test_result = [
|
||||
'status' => 'error'
|
||||
];
|
||||
|
||||
try {
|
||||
$test_result = \App\Misc\Mail::sendTestMail($request->to, $mailbox);
|
||||
$test_result = \MailHelper::sendTestMail($request->to, $mailbox);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
$test_result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
|
||||
if ($test_result['status'] == 'error') {
|
||||
$response['msg'] = $test_result['msg']
|
||||
?: __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
|
||||
$response['log'] = $test_result['log'] ?? '';
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
@@ -461,11 +658,23 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$response = \Eventy::filter('mailbox.fetch_test', $response, $mailbox);
|
||||
|
||||
$tested = (isset($response['tested']) && $response['tested'] === true);
|
||||
|
||||
// Check if outgoing port is open.
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$test_result = \Helper::checkPort($mailbox->in_server, $mailbox->in_port);
|
||||
if (!$test_result) {
|
||||
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->in_server.'</strong>', 'port' => '<strong>'.$mailbox->in_port.'</strong>']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$test_result = false;
|
||||
|
||||
try {
|
||||
@@ -475,11 +684,11 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend connecting to the server');
|
||||
$response['msg'] = __('Error occurred connecting to the server');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
@@ -490,7 +699,7 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
@@ -504,23 +713,33 @@ class MailboxesController extends Controller
|
||||
|
||||
$imap_folders = $client->getFolders();
|
||||
|
||||
$response['folders'] = [];
|
||||
|
||||
if (count($imap_folders)) {
|
||||
foreach ($imap_folders as $imap_folder) {
|
||||
if (!empty($imap_folder->name)) {
|
||||
$response['folders'][] = $imap_folder->name;
|
||||
}
|
||||
// Maybe we need a recursion here.
|
||||
if (!empty($imap_folder->children)) {
|
||||
foreach ($imap_folder->children as $child_imap_folder) {
|
||||
if (!empty($child_imap_folder->fullName)) {
|
||||
$response['folders'][] = $child_imap_folder->fullName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$response = $this->interateFolders($response, $imap_folders);
|
||||
$response['folders'] = array_values(array_unique($response['folders']));
|
||||
}
|
||||
|
||||
if (count($response['folders'])) {
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3933
|
||||
// Exclude duplicate INBOX.Name and Name folders.
|
||||
// $folder_excluded = false;
|
||||
// foreach ($response['folders'] as $i => $folder_name) {
|
||||
// if (\Str::startsWith($folder_name, 'INBOX.')) {
|
||||
// foreach ($response['folders'] as $folder_name2) {
|
||||
// if ($folder_name != $folder_name2 && $folder_name == 'INBOX.'.$folder_name2) {
|
||||
// unset($response['folders'][$i]);
|
||||
// $folder_excluded = true;
|
||||
// continue 2;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if ($folder_excluded) {
|
||||
// $response['folders'] = array_values($response['folders']);
|
||||
// }
|
||||
|
||||
$response['msg_success'] = __('IMAP folders retrieved: '.implode(', ', $response['folders']));
|
||||
} else {
|
||||
$response['msg_success'] = __('Connected, but no IMAP folders found');
|
||||
@@ -542,17 +761,22 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} elseif (!Hash::check($request->password, $user->password)) {
|
||||
} elseif (!$user->isDummyPassword() && !Hash::check($request->password ?? '', $user->password)) {
|
||||
$response['msg'] = __('Please double check your password, and try again');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
// Remove threads and conversations
|
||||
Thread::whereIn('conversation_id', $mailbox->conversations()->pluck('id')->toArray())
|
||||
->delete();
|
||||
// Remove threads and conversations.
|
||||
$conversation_ids = $mailbox->conversations()->pluck('id')->toArray();
|
||||
|
||||
for ($i=0; $i < ceil(count($conversation_ids) / \Helper::IN_LIMIT); $i++) {
|
||||
$slice_ids = array_slice($conversation_ids, $i*\Helper::IN_LIMIT, \Helper::IN_LIMIT);
|
||||
Thread::whereIn('conversation_id', $slice_ids)->delete();
|
||||
}
|
||||
|
||||
$mailbox->conversations()->delete();
|
||||
$mailbox->users()->sync([]);
|
||||
$mailbox->folders()->delete();
|
||||
@@ -566,15 +790,165 @@ class MailboxesController extends Controller
|
||||
}
|
||||
break;
|
||||
|
||||
// Mute notifications
|
||||
case 'mute':
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
if (!$mailbox_user) {
|
||||
// User may not be connected to the mailbox yet
|
||||
$user->mailboxes()->attach($mailbox->id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
}
|
||||
$mailbox_user->settings->mute = (bool)$request->mute;
|
||||
$mailbox_user->settings->save();
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occured';
|
||||
$response['msg'] = 'Unknown error occurred';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
// Recursively interate over folders.
|
||||
public function interateFolders($response, $imap_folders, $subfolder = false) {
|
||||
foreach ($imap_folders as $imap_folder) {
|
||||
if (!empty($imap_folder->name) && !$subfolder) {
|
||||
$response['folders'][] = $imap_folder->name;
|
||||
}
|
||||
|
||||
// Check for children and recurse.
|
||||
if (!empty($imap_folder->children)) {
|
||||
$response = $this->interateFolders($response, $imap_folder->children, true);
|
||||
}
|
||||
|
||||
// Old library.
|
||||
if (!empty($imap_folder->fullName)) {
|
||||
$response['folders'][] = $imap_folder->fullName;
|
||||
} elseif (!empty($imap_folder->full_name)) {
|
||||
// New library.
|
||||
$response['folders'][] = $imap_folder->full_name;
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function oauth(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
|
||||
$state_data = [];
|
||||
if (!empty($request->state)) {
|
||||
$state_data = json_decode($request->state, true);
|
||||
if (!empty($state_data['mailbox_id'])) {
|
||||
$mailbox_id = $state_data['mailbox_id'];
|
||||
}
|
||||
if (!empty($state_data['provider'])) {
|
||||
$provider = $state_data['provider'];
|
||||
}
|
||||
}
|
||||
|
||||
// MS Exchange.
|
||||
if (!empty($request->error) && $request->error == 'invalid_request' && !empty($request->error_description)) {
|
||||
return htmlspecialchars($request->error_description);
|
||||
}
|
||||
|
||||
if (empty($provider)) {
|
||||
return 'Invalid oAuth Provider';
|
||||
}
|
||||
|
||||
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
if (empty($mailbox)) {
|
||||
return __('Mailbox not found').': '.$mailbox_id;
|
||||
}
|
||||
if (empty($mailbox->in_username)) {
|
||||
return 'Enter oAuth Client ID as Username and save mailbox settings';
|
||||
}
|
||||
if (empty($mailbox->in_password)) {
|
||||
return 'Enter oAuth Client Secret as Password and save mailbox settings';
|
||||
}
|
||||
|
||||
$session_data = [];
|
||||
if (\Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id)) {
|
||||
$session_data = \Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||
}
|
||||
|
||||
if (empty($request->code)) {
|
||||
$state = [
|
||||
'provider' => $provider,
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'state' => crc32($mailbox->in_username.$mailbox->in_password),
|
||||
];
|
||||
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'state' => json_encode($state),
|
||||
'client_id' => $mailbox->in_username,
|
||||
]);
|
||||
if ($url) {
|
||||
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
|
||||
// [
|
||||
// 'provider' => $request->provider,
|
||||
// 'mailbox_id' => $request->mailbox_id,
|
||||
// 'state' => $provider->getState(),
|
||||
// ]);
|
||||
return redirect()->away($url);
|
||||
} else {
|
||||
return 'Could not generate authorization URL: check Client ID (Username) and Client Secret (Password)';
|
||||
}
|
||||
|
||||
// Check given state against previously stored one to mitigate CSRF attack
|
||||
} elseif (empty($request->state) || ($state_data['state'] ?? '') !== ($session_data['state'] ?? '')) {
|
||||
|
||||
\Session::forget('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||
return 'Invalid oAuth state';
|
||||
|
||||
} else {
|
||||
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
return __('Error occurred').': '.htmlspecialchars($token_data['error']);
|
||||
}
|
||||
|
||||
return redirect()->route('mailboxes.connection.incoming', ['id' => $mailbox_id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function oauthDisconnect(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
|
||||
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
// oAuth Disconnect.
|
||||
$mailbox->removeMetaParam('oauth', true);
|
||||
return \MailHelper::oauthDisconnect($provider, route('mailboxes.connection.incoming', ['id' => $mailbox_id]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,18 @@ class ModulesController extends Controller
|
||||
{
|
||||
$installed_modules = [];
|
||||
$modules_directory = [];
|
||||
$third_party_modules = [];
|
||||
$all_modules = [];
|
||||
$flashes = [];
|
||||
$updates_available = false;
|
||||
|
||||
$flash = \Cache::get('modules_flash');
|
||||
if ($flash) {
|
||||
$flashes[] = $flash;
|
||||
if (is_array($flash) && !isset($flash['text'])) {
|
||||
$flashes = $flash;
|
||||
} else {
|
||||
$flashes[] = $flash;
|
||||
}
|
||||
\Cache::forget('modules_flash');
|
||||
}
|
||||
|
||||
@@ -53,8 +59,7 @@ class ModulesController extends Controller
|
||||
\Module::clearCache();
|
||||
$modules = \Module::all();
|
||||
foreach ($modules as $module) {
|
||||
$img = '';
|
||||
$installed_modules[] = [
|
||||
$module_data = [
|
||||
'alias' => $module->getAlias(),
|
||||
'name' => $module->getName(),
|
||||
'description' => $module->getDescription(),
|
||||
@@ -65,14 +70,20 @@ class ModulesController extends Controller
|
||||
'requiredAppVersion' => $module->get('requiredAppVersion'),
|
||||
'requiredPhpExtensions' => $module->get('requiredPhpExtensions'),
|
||||
'requiredPhpExtensionsMissing' => \App\Module::getMissingExtensions($module->get('requiredPhpExtensions')),
|
||||
'img' => $img,
|
||||
'requiredModulesMissing' => \App\Module::getMissingModules($module->get('requiredModules'), $modules),
|
||||
'img' => $module->get('img'),
|
||||
'active' => $module->active(), //\App\Module::isActive($module->getAlias()),
|
||||
'installed' => true,
|
||||
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
|
||||
'license' => \App\Module::getLicense($module->getAlias()),
|
||||
// Update configuration for third party modules
|
||||
'latestVersionNumberUrl' => $module->get('latestVersionUrl'),
|
||||
'latestVersionZipUrl' => $module->get('latestVersionZipUrl'),
|
||||
// Determined later
|
||||
'new_version' => '',
|
||||
];
|
||||
$module_data = \App\Module::formatModuleData($module_data);
|
||||
$installed_modules[] = $module_data;
|
||||
}
|
||||
|
||||
// No need, as we update modules list on each page load
|
||||
@@ -84,10 +95,14 @@ class ModulesController extends Controller
|
||||
// Prepare directory modules
|
||||
if (is_array($modules_directory)) {
|
||||
foreach ($modules_directory as $i_dir => $dir_module) {
|
||||
|
||||
$modules_directory[$i_dir] = \App\Module::formatModuleData($dir_module);
|
||||
|
||||
// Remove modules without aliases
|
||||
if (empty($dir_module['alias'])) {
|
||||
unset($modules_directory[$i_dir]);
|
||||
}
|
||||
$all_modules[$dir_module['alias']] = $dir_module['name'];
|
||||
foreach ($installed_modules as $i_installed => $module) {
|
||||
if ($dir_module['alias'] == $module['alias']) {
|
||||
// Set image from director
|
||||
@@ -115,16 +130,76 @@ class ModulesController extends Controller
|
||||
}
|
||||
$modules_directory[$i_dir]['active'] = \App\Module::isActive($dir_module['alias']);
|
||||
$modules_directory[$i_dir]['activated'] = false;
|
||||
|
||||
// Do not show third-party modules in Modules Directory.
|
||||
if (\App\Module::isThirdParty($dir_module)) {
|
||||
$third_party_modules[] = $modules_directory[$i_dir];
|
||||
unset($modules_directory[$i_dir]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$modules_directory = [];
|
||||
}
|
||||
|
||||
// Loop through each installed module
|
||||
foreach ($installed_modules as $i_installed => $module) {
|
||||
// Check if the module is an official one
|
||||
if (\App\Module::isOfficial($module['authorUrl'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the URL for the latest version of the module
|
||||
$latest_version_number_url = $module['latestVersionNumberUrl'] ?? null;
|
||||
if (! $latest_version_number_url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a new Guzzle HTTP client
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
try {
|
||||
// Send a GET request to the latest version URL
|
||||
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
|
||||
|
||||
// Get the latest version number from the response body
|
||||
$latest_version = trim((string) $response->getBody());
|
||||
|
||||
if (empty($latest_version)) {
|
||||
continue;
|
||||
} else {
|
||||
// Get the current version of the module
|
||||
$current_version = $module['version'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If there's an exception, skip to the next iteration
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the latest version is greater than the current version
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// Update the installed module's version
|
||||
$installed_modules[ $i_installed ]['new_version'] = $latest_version;
|
||||
// Set the flag to indicate that updates are available
|
||||
$updates_available = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check modules symlinks. Somestimes instead of symlinks folders with files appear.
|
||||
$invalid_symlinks = \App\Module::checkSymlinks(
|
||||
collect($installed_modules)->where('active', true)->pluck('alias')->toArray()
|
||||
);
|
||||
|
||||
// Sort all modules.
|
||||
asort($all_modules);
|
||||
|
||||
return view('modules/modules', [
|
||||
'installed_modules' => $installed_modules,
|
||||
'modules_directory' => $modules_directory,
|
||||
'third_party_modules' => $third_party_modules,
|
||||
'flashes' => $flashes,
|
||||
'updates_available' => $updates_available,
|
||||
'all_modules' => $all_modules,
|
||||
'invalid_symlinks' => $invalid_symlinks,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -217,10 +292,10 @@ class ModulesController extends Controller
|
||||
\Session::flash('flash_error_floating', $response['msg']);
|
||||
}
|
||||
|
||||
\Session::flash('flash_error_unescaped', __('Error occured downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', ['%a_being%' => '<a href="'.$license_details['download_link'].'" target="_blank">', '%a_end%' => '</a>', 'folder' => '<strong>'.\Module::getPath().'</strong>']));
|
||||
\Session::flash('flash_error_unescaped', __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', ['%a_being%' => '<a href="'.$license_details['download_link'].'" target="_blank">', '%a_end%' => '</a>', 'folder' => '<strong>'.\Module::getPath().'</strong>']));
|
||||
}
|
||||
} else {
|
||||
$response['msg'] = __('Error occured. Please try again later.');
|
||||
$response['msg'] = __('Error occurred. Please try again later.');
|
||||
}
|
||||
} else {
|
||||
// Just activate license
|
||||
@@ -230,34 +305,9 @@ class ModulesController extends Controller
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
} elseif (!empty($result['error'])) {
|
||||
switch ($result['error']) {
|
||||
case 'missing':
|
||||
$response['msg'] = __('License key does not exist');
|
||||
break;
|
||||
case 'license_not_activable':
|
||||
$response['msg'] = __("You have to activate each bundle's module separately");
|
||||
break;
|
||||
case 'disabled':
|
||||
$response['msg'] = __('License key has been revoked');
|
||||
break;
|
||||
case 'no_activations_left':
|
||||
$response['msg'] = __('No activations left for this license key');
|
||||
break;
|
||||
case 'expired':
|
||||
$response['msg'] = __('License key has expired');
|
||||
break;
|
||||
case 'key_mismatch':
|
||||
$response['msg'] = __('License key belongs to another module');
|
||||
break;
|
||||
case 'invalid_item_id':
|
||||
$response['msg'] = __('Module not found in the modules directory');
|
||||
break;
|
||||
default:
|
||||
$response['msg'] = __('Error code:'.' '.$result['error']);
|
||||
break;
|
||||
}
|
||||
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
|
||||
} else {
|
||||
$response['msg'] = __('Error occured. Please try again later.');
|
||||
$response['msg'] = __('Error occurred. Please try again later.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,9 +350,25 @@ class ModulesController extends Controller
|
||||
case 'inactive':
|
||||
$response['msg'] = __('License key has not been activated yet');
|
||||
case 'site_inactive':
|
||||
$response['msg'] = __('This app has not been activated yet');
|
||||
$response['msg'] = __('No activations left for this license key').' ('.__("Use 'Deactivate License' link above to transfer license key from another domain").')';
|
||||
break;
|
||||
}
|
||||
} elseif (!empty($license_result['status']) && $license_result['status'] == 'inactive') {
|
||||
// Activate the license.
|
||||
$result = WpApi::activateLicense($params);
|
||||
if (WpApi::$lastError) {
|
||||
$response['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($result['code']) && !empty($result['message'])) {
|
||||
$response['msg'] = $result['message'];
|
||||
} else {
|
||||
if (!empty($result['status']) && $result['status'] == 'valid') {
|
||||
// Success.
|
||||
} elseif (!empty($result['error'])) {
|
||||
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
|
||||
} else {
|
||||
// Some unknown error. Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +387,7 @@ class ModulesController extends Controller
|
||||
}
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occured activating ":name" module', ['name' => $name]);
|
||||
$msg = __('Error occurred activating ":name" module', ['name' => $name]);
|
||||
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||
// If there was any error, module has been deactivated via modules.register_error filter
|
||||
$msg = '';
|
||||
@@ -332,11 +398,27 @@ class ModulesController extends Controller
|
||||
$type = 'success';
|
||||
$msg = __('":name" module successfully activated!', ['name' => $name]);
|
||||
} else {
|
||||
// Deactivate module
|
||||
// Deactivate the module.
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
|
||||
// Check public folder.
|
||||
if ($module && file_exists($module->getPath().DIRECTORY_SEPARATOR.'Public')) {
|
||||
$symlink_path = public_path().\Module::getPublicPath($alias);
|
||||
if (!file_exists($symlink_path)) {
|
||||
$type = 'danger';
|
||||
$msg = 'Error occurred creating a module symlink ('.$symlink_path.'). Please check folder permissions.';
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
}
|
||||
|
||||
if ($type == 'success') {
|
||||
// Migrate again, in case migration did not work in the moment the module was activated.
|
||||
\Artisan::call('migrate', ['--force' => true]);
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
$flash = [
|
||||
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||
@@ -365,10 +447,10 @@ class ModulesController extends Controller
|
||||
}
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occured deactivating :name module', ['name' => $name]);
|
||||
$msg = __('Error occurred deactivating :name module', ['name' => $name]);
|
||||
if (strstr($output, 'Configuration cached successfully')) {
|
||||
$type = 'success';
|
||||
$msg = __('":name" module successfully DEactivated!', ['name' => $name]);
|
||||
$msg = __('":name" module successfully Deactivated!', ['name' => $name]);
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
@@ -381,6 +463,56 @@ class ModulesController extends Controller
|
||||
$response['status'] = 'success';
|
||||
break;
|
||||
|
||||
case 'deactivate_license':
|
||||
$license = $request->license;
|
||||
$alias = $request->alias;
|
||||
|
||||
if (!$license) {
|
||||
$response['msg'] = __('Empty license key');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$params = [
|
||||
'license' => $license,
|
||||
'module_alias' => $alias,
|
||||
'url' => (!empty($request->any_url) ? '*' : \App\Module::getAppUrl()),
|
||||
];
|
||||
$result = WpApi::deactivateLicense($params);
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$response['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($result['code']) && !empty($result['message'])) {
|
||||
$response['msg'] = $result['message'];
|
||||
} else {
|
||||
if (!empty($result['status']) && $result['status'] == 'success') {
|
||||
$db_module = \App\Module::getByAlias($alias);
|
||||
if ($db_module && trim($db_module->license ?? '') == trim($license ?? '')) {
|
||||
// Remove remembered license key and deactivate license in DB
|
||||
\App\Module::deactivateLicense($alias, '');
|
||||
|
||||
// Deactivate module
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache', []);
|
||||
}
|
||||
|
||||
// Flash does not work here.
|
||||
$flash = [
|
||||
'text' => '<strong>'.__('License successfully Deactivated!').'</strong>',
|
||||
'unescaped' => true,
|
||||
'type' => 'success',
|
||||
];
|
||||
\Cache::forever('modules_flash', $flash);
|
||||
|
||||
$response['status'] = 'success';
|
||||
} elseif (!empty($result['error'])) {
|
||||
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
|
||||
} else {
|
||||
$response['msg'] = __('Error occurred. Please try again later.');
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$alias = $request->alias;
|
||||
|
||||
@@ -400,108 +532,34 @@ class ModulesController extends Controller
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
$alias = $request->alias;
|
||||
$module = \Module::findByAlias($alias);
|
||||
$update_result = \App\Module::updateModule($request->alias);
|
||||
|
||||
if (!$module) {
|
||||
$response['msg'] = __('Module not found').': '.$alias;
|
||||
}
|
||||
if ($update_result['download_error']) {
|
||||
$response['reload'] = true;
|
||||
|
||||
// Download new version
|
||||
$download_error = false;
|
||||
if (!$response['msg']) {
|
||||
$params = [
|
||||
'license' => \App\Module::getLicense($alias),
|
||||
'module_alias' => $alias,
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
];
|
||||
$license_details = WpApi::getVersion($params);
|
||||
if ($update_result['msg']) {
|
||||
\Session::flash('flash_error_floating', $update_result['msg']);
|
||||
}
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$response['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||
$response['msg'] = $license_details['message'];
|
||||
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
|
||||
$response['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
|
||||
} elseif (!empty($license_details['download_link'])) {
|
||||
// Download module
|
||||
$module_archive = \Module::getPath().DIRECTORY_SEPARATOR.$alias.'.zip';
|
||||
|
||||
try {
|
||||
\Helper::downloadRemoteFile($license_details['download_link'], $module_archive);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!file_exists($module_archive)) {
|
||||
$download_error = true;
|
||||
} else {
|
||||
// Extract
|
||||
try {
|
||||
\Helper::unzip($module_archive, \Module::getPath());
|
||||
} catch (\Exception $e) {
|
||||
// We will show this as floating message on page reload
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
// Check if extracted module exists
|
||||
\Module::clearCache();
|
||||
$module = \Module::findByAlias($alias);
|
||||
if (!$module) {
|
||||
$download_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove archive
|
||||
if (file_exists($module_archive)) {
|
||||
\File::delete($module_archive);
|
||||
}
|
||||
|
||||
if ($download_error) {
|
||||
$response['reload'] = true;
|
||||
|
||||
if ($response['msg']) {
|
||||
\Session::flash('flash_error_floating', $response['msg']);
|
||||
}
|
||||
|
||||
\Session::flash('flash_error_unescaped', __('Error occured downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', ['%a_being%' => '<a href="'.$license_details['download_link'].'" target="_blank">', '%a_end%' => '</a>', 'folder' => '<strong>'.\Module::getPath().'</strong>']));
|
||||
}
|
||||
} else {
|
||||
$response['msg'] = __('Error occured. Please try again later.');
|
||||
if ($update_result['download_msg']) {
|
||||
\Session::flash('flash_error_unescaped', $update_result['download_msg']);
|
||||
}
|
||||
}
|
||||
|
||||
// Install updated module
|
||||
if (!$response['msg'] && !$download_error) {
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
|
||||
// Get module name
|
||||
$name = '?';
|
||||
if ($module) {
|
||||
$name = $module->getName();
|
||||
}
|
||||
// Install updated module.
|
||||
if ($update_result['output'] || $update_result['status']) {
|
||||
|
||||
$type = 'danger';
|
||||
$msg = __('Error occured activating ":name" module', ['name' => $name]);
|
||||
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||
// If there was any error, module has been deactivated via modules.register_error filter
|
||||
$msg = '';
|
||||
foreach (session('flashes_floating') as $flash) {
|
||||
$msg .= $flash['text'].' ';
|
||||
}
|
||||
} elseif (strstr($output, 'Configuration cached successfully')) {
|
||||
$msg = $update_result['msg'];
|
||||
|
||||
if ($update_result['status'] == 'success') {
|
||||
$type = 'success';
|
||||
$msg = __('":name" module successfully updated!', ['name' => $name]);
|
||||
} else {
|
||||
// Deactivate module
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
$msg = $update_result['msg_success'];
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
$flash = [
|
||||
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$update_result['output'].'</pre>',
|
||||
'unescaped' => true,
|
||||
'type' => $type,
|
||||
];
|
||||
@@ -511,13 +569,48 @@ class ModulesController extends Controller
|
||||
|
||||
break;
|
||||
|
||||
case 'update_all':
|
||||
$update_all_flashes = [];
|
||||
|
||||
foreach ($request->aliases as $alias) {
|
||||
$update_result = \App\Module::updateModule($alias);
|
||||
|
||||
$type = 'danger';
|
||||
$msg = $update_result['msg'];
|
||||
|
||||
if ($update_result['status'] == 'success') {
|
||||
$type = 'success';
|
||||
$msg = $update_result['msg_success'];
|
||||
} elseif ($update_result['download_msg']) {
|
||||
$msg .= '<br/>'.$update_result['download_msg'];
|
||||
}
|
||||
|
||||
$text = '<strong>'.$update_result['module_name'].':</strong> '.$msg;
|
||||
if (trim($update_result['output'])) {
|
||||
$text .= '<pre class="margin-top">'.$update_result['output'].'</pre>';
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
$update_all_flashes[] = [
|
||||
'text' => $text,
|
||||
'unescaped' => true,
|
||||
'type' => $type,
|
||||
];
|
||||
}
|
||||
if ($update_all_flashes) {
|
||||
\Cache::forever('modules_flash', $update_all_flashes);
|
||||
}
|
||||
$response['status'] = 'success';
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occured';
|
||||
$response['msg'] = 'Unknown error occurred';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Attachment;
|
||||
use App\Conversation;
|
||||
use App\Option;
|
||||
use App\Thread;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Validator;
|
||||
|
||||
class PublicController extends Controller
|
||||
class OpenController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
@@ -33,7 +34,11 @@ class PublicController extends Controller
|
||||
}
|
||||
$user = User::where('invite_hash', $hash)->first();
|
||||
|
||||
return view('public/user_setup', ['user' => $user]);
|
||||
if ($user && $user->locale) {
|
||||
\Helper::setLocale($user->locale);
|
||||
}
|
||||
|
||||
return view('open/user_setup', ['user' => $user]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +76,7 @@ class PublicController extends Controller
|
||||
if ($path_url) {
|
||||
$user->photo_url = $path_url;
|
||||
} else {
|
||||
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
$validator->errors()->add('photo_url', __('Error occurred processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -96,7 +101,8 @@ class PublicController extends Controller
|
||||
|
||||
$user->invite_state = User::INVITE_STATE_ACTIVATED;
|
||||
$user->invite_hash = '';
|
||||
|
||||
|
||||
$user = \Eventy::filter('user.setup_save', $user, $request);
|
||||
$user->save();
|
||||
|
||||
// Login user
|
||||
@@ -119,10 +125,11 @@ class PublicController extends Controller
|
||||
if (empty($thread->opened_at)) {
|
||||
$thread->opened_at = date('Y-m-d H:i:s');
|
||||
$thread->save();
|
||||
\Eventy::action('thread.opened', $thread, $conversation);
|
||||
}
|
||||
|
||||
// Create a 1x1 ttransparent pixel and return it
|
||||
$pixel = sprintf('%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%', 71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 255, 0, 192, 192, 192, 0, 0, 0, 33, 249, 4, 1, 0, 0, 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59);
|
||||
$pixel = sprintf('%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c', 71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 255, 0, 192, 192, 192, 0, 0, 0, 33, 249, 4, 1, 0, 0, 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59);
|
||||
$response = \Response::make($pixel, 200);
|
||||
$response->header('Content-type', 'image/gif');
|
||||
$response->header('Content-Length', 42);
|
||||
@@ -133,4 +140,89 @@ class PublicController extends Controller
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an attachment.
|
||||
*/
|
||||
public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $request)
|
||||
{
|
||||
$id = $request->query('id', '');
|
||||
$token = $request->query('token', '');
|
||||
$attachment = null;
|
||||
|
||||
// Old attachments can not be requested by id.
|
||||
if (!$token && $id) {
|
||||
return \Helper::denyAccess();
|
||||
}
|
||||
|
||||
// Get attachment by id.
|
||||
if ($id) {
|
||||
$attachment = Attachment::findOrFail($id);
|
||||
}
|
||||
|
||||
if (!$attachment) {
|
||||
$attachment = Attachment::where('file_dir', $dir_1.DIRECTORY_SEPARATOR.$dir_2.DIRECTORY_SEPARATOR.$dir_3.DIRECTORY_SEPARATOR)
|
||||
->where('file_name', $file_name)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
// Only allow download if the attachment is public or if the token matches the hash of the contents
|
||||
if ($token != $attachment->getToken() && (bool)$attachment->public !== true) {
|
||||
return \Helper::denyAccess();
|
||||
}
|
||||
|
||||
$view_attachment = false;
|
||||
$file_ext = strtolower(pathinfo($attachment->file_name, PATHINFO_EXTENSION));
|
||||
|
||||
// Some file type should be viewed in the browser instead of downloading.
|
||||
if (in_array($file_ext, config('app.viewable_attachments'))) {
|
||||
$view_attachment = true;
|
||||
}
|
||||
// If HTML file is renamed into .txt for example it will be shown by the browser as HTML.
|
||||
if ($view_attachment && $attachment->mime_type) {
|
||||
$allowed_mime_type = false;
|
||||
|
||||
foreach (config('app.viewable_mime_types') as $mime_type) {
|
||||
if (preg_match('#'.$mime_type.'#', $attachment->mime_type)) {
|
||||
$allowed_mime_type = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$allowed_mime_type) {
|
||||
$view_attachment = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (config('app.download_attachments_via') == 'apache') {
|
||||
// Send using Apache mod_xsendfile.
|
||||
$response = response(null)
|
||||
->header('Content-Type' , $attachment->mime_type)
|
||||
->header('X-Sendfile', $attachment->getLocalFilePath());
|
||||
|
||||
if (!$view_attachment) {
|
||||
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||
}
|
||||
} elseif (config('app.download_attachments_via') == 'nginx') {
|
||||
// Send using Nginx.
|
||||
$response = response(null)
|
||||
->header('Content-Type' , $attachment->mime_type)
|
||||
->header('X-Accel-Redirect', $attachment->getLocalFilePath(false));
|
||||
|
||||
if (!$view_attachment) {
|
||||
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||
}
|
||||
} else {
|
||||
$response = $attachment->download($view_attachment);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Needed for the mobile app.
|
||||
*/
|
||||
// public function mobilePing()
|
||||
// {
|
||||
// echo file_get_contents(public_path('installer/css/fontawesome.css'));
|
||||
// }
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\ActivityLog;
|
||||
use App\Misc\Helper;
|
||||
use App\SendLog;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
@@ -27,7 +28,15 @@ class SecureController extends Controller
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
$mailboxes = auth()->user()->mailboxesCanView();
|
||||
$user = auth()->user();
|
||||
if (!$user->isAdmin()) {
|
||||
$mailboxes = $user->mailboxesCanView();
|
||||
} else {
|
||||
$mailboxes = $user->mailboxesCanViewWithSettings();
|
||||
}
|
||||
|
||||
// Sort by name.
|
||||
$mailboxes = \Eventy::filter('dashboard.mailboxes', $mailboxes->sortBy('name'));
|
||||
|
||||
return view('secure/dashboard', ['mailboxes' => $mailboxes]);
|
||||
}
|
||||
@@ -103,12 +112,16 @@ class SecureController extends Controller
|
||||
'type',
|
||||
'email',
|
||||
'status',
|
||||
'conversation',
|
||||
'message',
|
||||
'user',
|
||||
'customer',
|
||||
];
|
||||
|
||||
$activities = SendLog::orderBy('created_at', 'desc')->paginate($page_size);
|
||||
$activities_query = SendLog::orderBy('created_at', 'desc');
|
||||
if ($request->get('thread_id')) {
|
||||
$activities_query->where('thread_id', $request->get('thread_id'));
|
||||
}
|
||||
$activities = $activities_query->paginate($page_size);
|
||||
|
||||
foreach ($activities as $record) {
|
||||
$conversation = '';
|
||||
@@ -119,6 +132,12 @@ class SecureController extends Controller
|
||||
$status = $record->getStatusName();
|
||||
if ($record->status_message) {
|
||||
$status .= '. '.$record->status_message;
|
||||
if ($record->status == SendLog::STATUS_SEND_ERROR) {
|
||||
$status .= '. Message-ID: '.$record->message_id;
|
||||
}
|
||||
}
|
||||
if ($record->smtp_queue_id) {
|
||||
$status .= '. SMTP ID: '.$record->smtp_queue_id;
|
||||
}
|
||||
|
||||
$logs[] = [
|
||||
@@ -126,7 +145,7 @@ class SecureController extends Controller
|
||||
'type' => $record->getMailTypeName(),
|
||||
'email' => $record->email,
|
||||
'status' => $status,
|
||||
'conversation' => $conversation,
|
||||
'message' => $conversation,
|
||||
'user' => $record->user,
|
||||
'customer' => $record->customer,
|
||||
];
|
||||
@@ -158,7 +177,7 @@ class SecureController extends Controller
|
||||
|
||||
$name = '';
|
||||
if (!empty($request->name)) {
|
||||
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
|
||||
//$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
|
||||
$name = $request->name;
|
||||
} elseif (count($names = ActivityLog::select('log_name')->distinct()->get()->pluck('log_name'))) {
|
||||
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||
@@ -177,4 +196,41 @@ class SecureController extends Controller
|
||||
|
||||
return redirect()->route('logs', ['name' => $name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files and images.
|
||||
*/
|
||||
public function upload(Request $request, $allowed_exts = [])
|
||||
{
|
||||
// 'jpg','gif','png'
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'msg' => '', // this is error message
|
||||
];
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
$response['msg'] = __('Please login to upload file');
|
||||
}
|
||||
|
||||
if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) {
|
||||
$response['msg'] = __('Error occurred uploading file');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$upload = Helper::uploadFile($request->file, $allowed_exts);
|
||||
$filename = basename($upload);
|
||||
|
||||
if ($upload) {
|
||||
$response['status'] = 'success';
|
||||
$response['url'] = Helper::uploadedFileUrl($filename);
|
||||
} else {
|
||||
$response['msg'] = __('Error occurred uploading file');
|
||||
}
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Option;
|
||||
use App\Subscription;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Validator;
|
||||
@@ -99,25 +100,58 @@ class SettingsController extends Controller
|
||||
'fetch_schedule' => [
|
||||
'env' => 'APP_FETCH_SCHEDULE',
|
||||
],
|
||||
'mail_password' => [
|
||||
'safe_password' => true,
|
||||
'encrypt' => true,
|
||||
],
|
||||
// 'use_mail_date_on_fetching' => [
|
||||
// 'env' => 'APP_USE_MAIL_DATE_ON_FETCHING',
|
||||
// ],
|
||||
],
|
||||
];
|
||||
break;
|
||||
case 'general':
|
||||
$params = [
|
||||
'settings' => [
|
||||
'custom_number' => [
|
||||
'env' => 'APP_CUSTOM_NUMBER',
|
||||
],
|
||||
'max_message_size' => [
|
||||
'env' => 'APP_MAX_MESSAGE_SIZE',
|
||||
],
|
||||
'email_conv_history' => [
|
||||
'env' => 'APP_EMAIL_CONV_HISTORY',
|
||||
],
|
||||
'email_user_history' => [
|
||||
'env' => 'APP_EMAIL_USER_HISTORY',
|
||||
],
|
||||
'locale' => [
|
||||
'env' => 'APP_LOCALE',
|
||||
],
|
||||
'timezone' => [
|
||||
'env' => 'APP_TIMEZONE',
|
||||
],
|
||||
'user_permissions' => [
|
||||
'env' => 'APP_USER_PERMISSIONS',
|
||||
'env_encode' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
break;
|
||||
case 'alerts':
|
||||
$subscriptions_defaults = Subscription::getDefaultSubscriptions();
|
||||
$subscriptions = array();
|
||||
foreach ($subscriptions_defaults as $medium => $subscriptions_for_medium) {
|
||||
foreach ($subscriptions_defaults[$medium] as $subscription) {
|
||||
$subscriptions[] = (object) array("medium" => $medium, "event" => $subscription);
|
||||
}
|
||||
}
|
||||
$params = [
|
||||
'template_vars' => [
|
||||
'logs' => \App\ActivityLog::getAvailableLogs(),
|
||||
'person' => null,
|
||||
'subscriptions' => $subscriptions,
|
||||
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
|
||||
],
|
||||
'settings' => [
|
||||
'alert_logs' => [
|
||||
@@ -142,6 +176,8 @@ class SettingsController extends Controller
|
||||
break;
|
||||
}
|
||||
|
||||
$params = \Eventy::filter('settings.alter_section_params', $params, $section);
|
||||
|
||||
if ($param) {
|
||||
if (isset($params[$param])) {
|
||||
return $params[$param];
|
||||
@@ -162,9 +198,13 @@ class SettingsController extends Controller
|
||||
$settings = [
|
||||
'company_name' => Option::get('company_name', \Config::get('app.name')),
|
||||
'next_ticket' => (Option::get('next_ticket') >= Conversation::max('number') + 1) ? Option::get('next_ticket') : Conversation::max('number') + 1,
|
||||
'user_permissions' => Option::get('user_permissions', []),
|
||||
'custom_number' => (int)config('app.custom_number'),
|
||||
'user_permissions' => User::getGlobalUserPermissions(),
|
||||
'email_branding' => Option::get('email_branding'),
|
||||
'open_tracking' => Option::get('open_tracking'),
|
||||
'email_conv_history' => config('app.email_conv_history'),
|
||||
'max_message_size' => config('app.max_message_size'),
|
||||
'email_user_history' => config('app.email_user_history'),
|
||||
'enrich_customer_data' => Option::get('enrich_customer_data'),
|
||||
'time_format' => Option::get('time_format', User::TIME_FORMAT_24),
|
||||
'locale' => \Helper::getRealAppLocale(),
|
||||
@@ -178,9 +218,10 @@ class SettingsController extends Controller
|
||||
'mail_host' => Option::get('mail_host', \Config::get('mail.host')),
|
||||
'mail_port' => Option::get('mail_port', \Config::get('mail.port')),
|
||||
'mail_username' => Option::get('mail_username', \Config::get('mail.username')),
|
||||
'mail_password' => Option::get('mail_password', \Config::get('mail.password')),
|
||||
'mail_password' => \Helper::decrypt(Option::get('mail_password', \Config::get('mail.password'))),
|
||||
'mail_encryption' => Option::get('mail_encryption', \Config::get('mail.encryption')),
|
||||
'fetch_schedule' => config('app.fetch_schedule'),
|
||||
//'use_mail_date_on_fetching' => config('app.use_mail_date_on_fetching'),
|
||||
];
|
||||
break;
|
||||
case 'alerts':
|
||||
@@ -191,6 +232,7 @@ class SettingsController extends Controller
|
||||
'alert_logs',
|
||||
'alert_logs_names',
|
||||
'alert_logs_period',
|
||||
'subscription_defaults',
|
||||
], [
|
||||
'alert_logs_names' => [],
|
||||
'alert_logs' => config('app.alert_logs'),
|
||||
@@ -202,6 +244,8 @@ class SettingsController extends Controller
|
||||
break;
|
||||
}
|
||||
|
||||
$settings = \Eventy::filter('settings.alter_section_settings', $settings, $section);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -252,23 +296,56 @@ class SettingsController extends Controller
|
||||
$cc_required = false;
|
||||
$settings_params = $this->getSectionParams($section, 'settings');
|
||||
foreach ($settings as $i => $option_name) {
|
||||
// Do not save dummy passwords.
|
||||
if (!empty($settings_params[$option_name])
|
||||
&& !empty($settings_params[$option_name]['safe_password'])
|
||||
&& $request->settings[$option_name]
|
||||
&& preg_match("/^\*+$/", $request->settings[$option_name])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Option has to be saved to .env file.
|
||||
if (!empty($settings_params[$option_name]) && !empty($settings_params[$option_name]['env'])) {
|
||||
$env_value = $request->settings[$option_name] ?? '';
|
||||
|
||||
if (is_array($env_value)) {
|
||||
$env_value = json_encode($env_value);
|
||||
}
|
||||
|
||||
if (!empty($settings_params[$option_name]['encrypt'])) {
|
||||
$env_value = encrypt($env_value);
|
||||
}
|
||||
|
||||
if (!empty($settings_params[$option_name]['env_encode'])) {
|
||||
$env_value = base64_encode($env_value);
|
||||
}
|
||||
|
||||
\Helper::setEnvFileVar($settings_params[$option_name]['env'], $env_value);
|
||||
|
||||
config($option_name, $env_value);
|
||||
$cc_required = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// By some reason isset() does not work for empty elements.
|
||||
if (array_key_exists($option_name, $request->settings)) {
|
||||
if (isset($request->settings) && array_key_exists($option_name, $request->settings)) {
|
||||
$option_value = $request->settings[$option_name];
|
||||
|
||||
if (!empty($settings_params[$option_name]['encrypt'])) {
|
||||
$option_value = encrypt($option_value);
|
||||
}
|
||||
|
||||
Option::set($option_name, $option_value);
|
||||
} else {
|
||||
// If option does not exist, default will be used,
|
||||
// so we can not just remove bool settings.
|
||||
if (\Option::getDefault($option_name, null) === true) {
|
||||
if (isset($settings_params[$option_name]['default'])) {
|
||||
$default = $settings_params[$option_name]['default'];
|
||||
} else {
|
||||
$default = \Option::getDefault($option_name, null);
|
||||
}
|
||||
if ($default === true) {
|
||||
Option::set($option_name, false);
|
||||
} elseif (is_array(\Option::getDefault($option_name, -1))) {
|
||||
Option::set($option_name, []);
|
||||
@@ -279,13 +356,20 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
// Clear cache if some options have been saved to .env file.
|
||||
// Clearing the cache also restarts queue:work as it also
|
||||
// needs to get new .env parameters.
|
||||
if ($cc_required) {
|
||||
\Helper::clearCache(['--doNotGenerateVars' => true]);
|
||||
}
|
||||
|
||||
\Session::flash('flash_success_floating', __('Settings updated'));
|
||||
// \Helper::clearCache prevents \Session::flash() from displaying.
|
||||
$request->session()->flash('flash_success_floating', __('Settings updated'));
|
||||
|
||||
return redirect()->route('settings', ['section' => $section]);
|
||||
$response = redirect()->route('settings', ['section' => $section]);
|
||||
|
||||
$response = \Eventy::filter('settings.after_save', $response, $request, $section, $settings);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,17 +394,22 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$test_result = false;
|
||||
$test_result = [
|
||||
'status' => 'error'
|
||||
];
|
||||
|
||||
try {
|
||||
$test_result = \MailHelper::sendTestMail($request->to);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
$test_result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
|
||||
if ($test_result['status'] == 'error') {
|
||||
$response['msg'] = $test_result['msg']
|
||||
?: __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
|
||||
$response['log'] = $test_result['log'] ?? '';
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
@@ -339,7 +428,7 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occured';
|
||||
$response['msg'] = 'Unknown error occurred';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
|
||||
@@ -4,12 +4,15 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Option;
|
||||
use App\User;
|
||||
use App\FailedJob;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class SystemController extends Controller
|
||||
{
|
||||
public static $latest_version_error = '';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
@@ -17,7 +20,9 @@ class SystemController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('auth', ['except' => [
|
||||
'cron'
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,21 +30,11 @@ class SystemController extends Controller
|
||||
*/
|
||||
public function status(Request $request)
|
||||
{
|
||||
// PHP extensions
|
||||
$php_extensions = [];
|
||||
foreach (\Config::get('app.required_extensions') as $extension_name) {
|
||||
$alternatives = explode('/', $extension_name);
|
||||
if ($alternatives) {
|
||||
foreach ($alternatives as $alternative) {
|
||||
$php_extensions[$extension_name] = extension_loaded(trim($alternative));
|
||||
if ($php_extensions[$extension_name]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$php_extensions[$extension_name] = extension_loaded($extension_name);
|
||||
}
|
||||
}
|
||||
// PHP extensions.
|
||||
$php_extensions = \Helper::checkRequiredExtensions();
|
||||
|
||||
// Functions.
|
||||
$functions = \Helper::checkRequiredFunctions();
|
||||
|
||||
// Permissions.
|
||||
$permissions = [];
|
||||
@@ -54,6 +49,20 @@ class SystemController extends Controller
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
// Check if cache files are writable.
|
||||
$non_writable_cache_file = '';
|
||||
if (function_exists('shell_exec')) {
|
||||
$non_writable_cache_file = \Helper::shellExec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
|
||||
$non_writable_cache_file = trim($non_writable_cache_file ?? '');
|
||||
// Leave only one line (in case head -n 1 does not work)
|
||||
$non_writable_cache_file = preg_replace("#[\r\n].+#m", '', $non_writable_cache_file);
|
||||
if (!strstr($non_writable_cache_file, base_path('storage/framework/cache/data/'))) {
|
||||
$non_writable_cache_file = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if public symlink exists, if not, try to create.
|
||||
$public_symlink_exists = true;
|
||||
$public_path = public_path('storage');
|
||||
@@ -76,8 +85,11 @@ class SystemController extends Controller
|
||||
$failed_queues = $failed_jobs->pluck('queue')->unique();
|
||||
|
||||
// Commands
|
||||
$commands_list = ['freescout:fetch-emails', 'queue:work'];
|
||||
foreach ($commands_list as $command_name) {
|
||||
$commands_list = [
|
||||
'freescout:fetch-emails' => 'freescout:fetch-emails',
|
||||
\Helper::getWorkerIdentifier() => 'queue:work'
|
||||
];
|
||||
foreach ($commands_list as $command_identifier => $command_name) {
|
||||
$status_texts = [];
|
||||
|
||||
// Check if command is running now
|
||||
@@ -85,10 +97,16 @@ class SystemController extends Controller
|
||||
$running_commands = 0;
|
||||
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_name}'"));
|
||||
$processes = preg_split("/[\r\n]/", \Helper::shellExec("ps aux | grep '{$command_identifier}'"));
|
||||
$pids = [];
|
||||
foreach ($processes as $process) {
|
||||
$process = trim($process);
|
||||
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
|
||||
if (empty($m)) {
|
||||
// Another format (used in Docker image).
|
||||
// 1713 nginx 0:00 /usr/bin/php82...
|
||||
preg_match("/^([\d]+)\s+[\S]+\s+/", $process, $m);
|
||||
}
|
||||
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
|
||||
$running_commands++;
|
||||
$pids[] = $m[1];
|
||||
@@ -107,7 +125,7 @@ class SystemController extends Controller
|
||||
} elseif ($running_commands > 1) {
|
||||
// queue:work command is stopped by settings a cache key
|
||||
if ($command_name == 'queue:work') {
|
||||
\Helper::queueWorkRestart();
|
||||
\Helper::queueWorkerRestart();
|
||||
$commands[] = [
|
||||
'name' => $command_name,
|
||||
'status' => 'error',
|
||||
@@ -163,33 +181,63 @@ class SystemController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
// Check new version
|
||||
// Check new version if enabled
|
||||
$new_version_available = false;
|
||||
$latest_version = \Cache::remember('latest_version', 15, function () {
|
||||
return \Updater::getVersionAvailable();
|
||||
});
|
||||
if (!\Config::get('app.disable_updating')) {
|
||||
$latest_version = \Cache::remember('latest_version', 15, function () {
|
||||
try {
|
||||
return \Updater::getVersionAvailable();
|
||||
} catch (\Exception $e) {
|
||||
SystemController::$latest_version_error = $e->getMessage();
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
|
||||
$new_version_available = true;
|
||||
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
|
||||
$new_version_available = true;
|
||||
}
|
||||
} else {
|
||||
$latest_version = \Config::get('app.version');
|
||||
}
|
||||
|
||||
// Detect missing migrations.
|
||||
$migrations_output = \Helper::runCommand('migrate:status');
|
||||
preg_match_all("#\| N \| ([^\|]+)\|#", $migrations_output, $migrations_m);
|
||||
$missing_migrations = $migrations_m[1] ?? [];
|
||||
|
||||
return view('system/status', [
|
||||
'commands' => $commands,
|
||||
'queued_jobs' => $queued_jobs,
|
||||
'failed_jobs' => $failed_jobs,
|
||||
'failed_queues' => $failed_queues,
|
||||
'php_extensions' => $php_extensions,
|
||||
'functions' => $functions,
|
||||
'permissions' => $permissions,
|
||||
'new_version_available' => $new_version_available,
|
||||
'latest_version' => $latest_version,
|
||||
'latest_version_error' => SystemController::$latest_version_error,
|
||||
'public_symlink_exists' => $public_symlink_exists,
|
||||
'env_is_writable' => $env_is_writable,
|
||||
'non_writable_cache_file' => $non_writable_cache_file,
|
||||
'missing_migrations' => $missing_migrations,
|
||||
'invalid_symlinks' => \App\Module::checkSymlinks(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function action(Request $request)
|
||||
{
|
||||
switch ($request->action) {
|
||||
case 'cancel_job':
|
||||
\App\Job::where('id', $request->job_id)->delete();
|
||||
\Session::flash('flash_success_floating', __('Done'));
|
||||
break;
|
||||
|
||||
case 'retry_job':
|
||||
\App\Job::where('id', $request->job_id)->update(['available_at' => time()]);
|
||||
sleep(1);
|
||||
\Session::flash('flash_success_floating', __('Done'));
|
||||
break;
|
||||
|
||||
case 'delete_failed_jobs':
|
||||
\App\FailedJob::where('queue', $request->failed_queue)->delete();
|
||||
\Session::flash('flash_success_floating', __('Failed jobs deleted'));
|
||||
@@ -248,6 +296,10 @@ class SystemController extends Controller
|
||||
case 'migrate_db':
|
||||
\Artisan::call('migrate', ['--force' => true], $outputLog);
|
||||
break;
|
||||
|
||||
case 'logout_users':
|
||||
\Artisan::call('freescout:logout-users', [], $outputLog);
|
||||
break;
|
||||
}
|
||||
|
||||
$output = $outputLog->fetch();
|
||||
@@ -276,10 +328,10 @@ class SystemController extends Controller
|
||||
case 'update':
|
||||
try {
|
||||
$status = \Updater::update();
|
||||
|
||||
|
||||
// Artisan::output()
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured. Please try again or try another :%a_start%update method:%a_end%', ['%a_start%' => '<a href="'.config('app.freescout_url').'/docs/update/" target="_blank">', '%a_end%' => '</a>']);
|
||||
$response['msg'] = __('Error occurred. Please try again or try another :%a_start%update method:%a_end%', ['%a_start%' => '<a href="'.config('app.freescout_url').'/docs/update/" target="_blank">', '%a_end%' => '</a>']);
|
||||
$response['msg'] .= '<br/><br/>'.$e->getMessage();
|
||||
|
||||
\Helper::logException($e);
|
||||
@@ -292,14 +344,18 @@ class SystemController extends Controller
|
||||
break;
|
||||
|
||||
case 'check_updates':
|
||||
try {
|
||||
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured').': '.$e->getMessage();
|
||||
}
|
||||
if (!$response['msg'] && !$response['new_version_available']) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
if (!\Config::get('app.disable_updating')) {
|
||||
try {
|
||||
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occurred').': '.$e->getMessage();
|
||||
}
|
||||
if (!$response['msg'] && !$response['new_version_available']) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
$response['msg_success'] = __('You have the latest version installed');
|
||||
}
|
||||
} else {
|
||||
$response['msg_success'] = __('You have the latest version installed');
|
||||
}
|
||||
break;
|
||||
@@ -310,9 +366,51 @@ class SystemController extends Controller
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occured';
|
||||
$response['msg'] = 'Unknown error occurred';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Cron.
|
||||
*/
|
||||
public function cron(Request $request)
|
||||
{
|
||||
if (empty($request->hash) || $request->hash != \Helper::getWebCronHash()) {
|
||||
abort(404);
|
||||
}
|
||||
$outputLog = new BufferedOutput();
|
||||
\Artisan::call('schedule:run', [], $outputLog);
|
||||
$output = $outputLog->fetch();
|
||||
|
||||
return response($output, 200)->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax HTML.
|
||||
*/
|
||||
public function ajaxHtml(Request $request)
|
||||
{
|
||||
switch ($request->action) {
|
||||
case 'job_details':
|
||||
$job = \App\FailedJob::find($request->param);
|
||||
if (!$job) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$payload = json_decode($job->payload, true);
|
||||
|
||||
if (!empty($payload['data']['command'])) {
|
||||
$html .= '<pre>'.print_r(unserialize($payload['data']['command']), 1).'</pre>';
|
||||
}
|
||||
|
||||
$html .= '<pre>'.$job->exception.'</pre>';
|
||||
|
||||
return response($html);
|
||||
}
|
||||
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use \Barryvdh\TranslationManager\Models\Translation;
|
||||
use Barryvdh\TranslationManager\Controller as BaseController;
|
||||
|
||||
class TranslateController extends BaseController
|
||||
@@ -15,16 +16,30 @@ class TranslateController extends BaseController
|
||||
{
|
||||
$result = false;
|
||||
|
||||
// Count changed translations.
|
||||
$changed_data = Translation::select(['locale', 'group', \DB::raw('count(*) as changed')])
|
||||
->where('status', Translation::STATUS_CHANGED)
|
||||
->groupBy(['locale', 'group'])
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$this->manager->exportTranslations('*', false);
|
||||
|
||||
// Archive langs folder
|
||||
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
|
||||
try {
|
||||
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'error_msg' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($archive_path) {
|
||||
$attachments[] = $archive_path;
|
||||
|
||||
// Send archive to developers
|
||||
$result = \MailHelper::sendEmailToDevs('Translations', '', $attachments, auth()->user());
|
||||
$result = \MailHelper::sendEmailToDevs('Translations', json_encode($changed_data), $attachments, auth()->user());
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
@@ -67,4 +82,13 @@ class TranslateController extends BaseController
|
||||
|
||||
return \Response::download($public_path, $file_name, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of strings to translate.
|
||||
*/
|
||||
public function stringsToTranslate()
|
||||
{
|
||||
__(':field is required');
|
||||
__('The following modules have to be installed and activated: :modules');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class UsersController extends Controller
|
||||
$this->authorize('create', 'App\User');
|
||||
|
||||
$users = User::nonDeleted()->get();
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
return view('users/users', ['users' => $users]);
|
||||
}
|
||||
@@ -56,21 +57,30 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$this->authorize('create', 'App\User');
|
||||
$auth_user = auth()->user();
|
||||
|
||||
$rules = [
|
||||
'role' => 'integer',
|
||||
'first_name' => 'required|string|max:20',
|
||||
'last_name' => 'required|string|max:30',
|
||||
'email' => 'required|string|email|max:100|unique:users',
|
||||
'role' => ['required', Rule::in(array_keys(User::$roles))],
|
||||
//'role' => ['required', Rule::in(array_keys(User::$roles))],
|
||||
];
|
||||
if ($auth_user->isAdmin()) {
|
||||
$rules['role'] = ['required', Rule::in(array_keys(User::$roles))];
|
||||
}
|
||||
if (empty($request->send_invite)) {
|
||||
$rules['password'] = 'required|string|max:255';
|
||||
}
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (User::mailboxEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('users.create')
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
@@ -78,6 +88,9 @@ class UsersController extends Controller
|
||||
|
||||
$user = new User();
|
||||
$user->fill($request->all());
|
||||
if (!$auth_user->can('changeRole', $user)) {
|
||||
$user->role = User::ROLE_USER;
|
||||
}
|
||||
if (empty($request->send_invite)) {
|
||||
// Set password from request
|
||||
$user->password = Hash::make($request->password);
|
||||
@@ -85,9 +98,12 @@ class UsersController extends Controller
|
||||
// Set some random password before sending invite
|
||||
$user->password = Hash::make($user->generateRandomPassword());
|
||||
}
|
||||
// Set system timezone.
|
||||
$user->timezone = config('app.timezone') ?: User::DEFAULT_TIMEZONE;
|
||||
$user = \Eventy::filter('user.create_save', $user, $request);
|
||||
$user->save();
|
||||
|
||||
$user->mailboxes()->sync($request->mailboxes);
|
||||
$user->mailboxes()->sync($request->mailboxes ?: []);
|
||||
$user->syncPersonalFolders($request->mailboxes);
|
||||
|
||||
// Send invite
|
||||
@@ -96,7 +112,7 @@ class UsersController extends Controller
|
||||
$user->sendInvite(true);
|
||||
} catch (\Exception $e) {
|
||||
// Admin is allowed to see exceptions
|
||||
\Session::flash('flash_error_floating', $e->getMessage());
|
||||
\Session::flash('flash_error_floating', $e->getMessage().' — '.__('Check mail settings in "Manage » Settings » Mail Settings"'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +141,7 @@ class UsersController extends Controller
|
||||
public function getUsersForSidebar($except_id)
|
||||
{
|
||||
if (auth()->user()->isAdmin()) {
|
||||
return User::all()->except($except_id);
|
||||
return User::sortUsers(User::nonDeleted()->get());/*->except($except_id)*/;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -140,6 +156,8 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function profileSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
$this->authorize('update', $user);
|
||||
|
||||
@@ -148,7 +166,7 @@ class UsersController extends Controller
|
||||
'first_name' => 'required|string|max:20',
|
||||
'last_name' => 'required|string|max:30',
|
||||
'email' => 'required|string|email|max:100|unique:users,email,'.$id,
|
||||
'emails' => 'max:100',
|
||||
//'emails' => 'max:100',
|
||||
'job_title' => 'max:100',
|
||||
'phone' => 'max:60',
|
||||
'timezone' => 'required|string|max:255',
|
||||
@@ -168,7 +186,8 @@ class UsersController extends Controller
|
||||
if ($path_url) {
|
||||
$user->photo_url = $path_url;
|
||||
} else {
|
||||
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
$invalid = true;
|
||||
$validator->errors()->add('photo_url', __('Error occurred processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,12 +195,18 @@ class UsersController extends Controller
|
||||
if ($user->isAdmin() && isset($request->role) && $request->role != User::ROLE_ADMIN) {
|
||||
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
|
||||
if ($admins_count < 2) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('role', __('Role of the only one administrator can not be changed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (User::mailboxEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('users.profile', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
@@ -200,12 +225,21 @@ class UsersController extends Controller
|
||||
if (!auth()->user()->can('changeRole', $user)) {
|
||||
unset($request_data['role']);
|
||||
}
|
||||
$user->fill($request_data);
|
||||
if ($user->status != User::STATUS_DELETED) {
|
||||
if (!empty($request_data['disabled'])) {
|
||||
$request_data['status'] = User::STATUS_DISABLED;
|
||||
} else {
|
||||
$request_data['status'] = User::STATUS_ACTIVE;
|
||||
}
|
||||
}
|
||||
$user->setData($request_data);
|
||||
|
||||
if (empty($request->input('enable_kb_shortcuts'))) {
|
||||
$user->enable_kb_shortcuts = false;
|
||||
}
|
||||
|
||||
$user = \Eventy::filter('user.save_profile', $user, $request);
|
||||
|
||||
$user->save();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Profile saved successfully'));
|
||||
@@ -252,9 +286,24 @@ class UsersController extends Controller
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$user->mailboxes()->sync($request->mailboxes);
|
||||
$user->mailboxes()->sync($request->mailboxes ?: []);
|
||||
$user->syncPersonalFolders($request->mailboxes);
|
||||
|
||||
// Save permissions.
|
||||
$user_permissions = $request->user_permissions ?? [];
|
||||
$permissions = [];
|
||||
|
||||
foreach (User::getUserPermissionsList() as $permission_id) {
|
||||
$new_has_permission = in_array($permission_id, $user_permissions);
|
||||
|
||||
if ($user->hasPermission($permission_id, false) != $new_has_permission) {
|
||||
$permissions[$permission_id] = (int)(bool)$new_has_permission;
|
||||
$save_user = true;
|
||||
}
|
||||
}
|
||||
$user->permissions = $permissions;
|
||||
$user->save();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Permissions saved successfully'));
|
||||
|
||||
return redirect()->route('users.permissions', ['id' => $id]);
|
||||
@@ -282,6 +331,7 @@ class UsersController extends Controller
|
||||
'subscriptions' => $subscriptions,
|
||||
'person' => $person,
|
||||
'users' => $users,
|
||||
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -340,8 +390,8 @@ class UsersController extends Controller
|
||||
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
// Admin is allowed to see exceptions
|
||||
$response['msg'] = $e->getMessage();
|
||||
// Admin is allowed to see exceptions.
|
||||
$response['msg'] = $e->getMessage().' — '.__('Check mail settings in "Manage » Settings » Mail Settings"');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -440,7 +490,6 @@ class UsersController extends Controller
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
event(new UserDeleted($user, $auth_user));
|
||||
|
||||
// We have to process conversations one by one to move them to Unassigned folder,
|
||||
// as conversations may be in different mailboxes
|
||||
@@ -449,30 +498,42 @@ class UsersController extends Controller
|
||||
|
||||
$user->conversations->each(function ($conversation) use ($auth_user, $request) {
|
||||
// We don't fire ConversationUserChanged event to avoid sending notifications to users
|
||||
if (!empty($request->assign_user) && !empty($request->assign_user[$conversation->mailbox_id]) && (int) $request->assign_user[$conversation->mailbox_id] != -1) {
|
||||
if (!empty($request->assign_user)
|
||||
&& !empty($request->assign_user[$conversation->mailbox_id])
|
||||
&& (int) $request->assign_user[$conversation->mailbox_id] != -1
|
||||
) {
|
||||
// Set assignee.
|
||||
// In this case conversation stays assigned, just assignee changes.
|
||||
$conversation->user_id = $request->assign_user[$conversation->mailbox_id];
|
||||
// In this case conversation stays assigned, just assignee changes.
|
||||
|
||||
} else {
|
||||
// Set assignee.
|
||||
|
||||
// Make convesation Unassigned.
|
||||
|
||||
// Unset assignee.
|
||||
// Maybe use changeUser() here.
|
||||
$conversation->user_id = null;
|
||||
|
||||
// Change conversation folder to ANASSIGNED.
|
||||
$folder_id = null;
|
||||
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
|
||||
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
|
||||
} else {
|
||||
$folder = $conversation->mailbox->folders()
|
||||
->where('type', Folder::TYPE_UNASSIGNED)
|
||||
->first();
|
||||
if ($conversation->isPublished()
|
||||
&& ($conversation->isActive() || $conversation->isPending())
|
||||
) {
|
||||
// Change conversation folder to UNASSIGNED.
|
||||
$folder_id = null;
|
||||
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
|
||||
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
|
||||
} else {
|
||||
$folder = $conversation->mailbox->folders()
|
||||
->where('type', Folder::TYPE_UNASSIGNED)
|
||||
->first();
|
||||
|
||||
if ($folder) {
|
||||
$folder_id = $folder->id;
|
||||
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
|
||||
if ($folder) {
|
||||
$folder_id = $folder->id;
|
||||
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
|
||||
}
|
||||
}
|
||||
if ($folder_id) {
|
||||
$conversation->folder_id = $folder_id;
|
||||
}
|
||||
}
|
||||
if ($folder_id) {
|
||||
$conversation->folder_id = $folder_id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,12 +572,14 @@ class UsersController extends Controller
|
||||
|
||||
$user->status = \App\User::STATUS_DELETED;
|
||||
// Update email.
|
||||
$email_suffix = '_deleted'.date('YmdHis');
|
||||
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
|
||||
// We have to truncate email to avoid "Data too long" error.
|
||||
$user->email = mb_substr($user->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;
|
||||
|
||||
$user->save();
|
||||
|
||||
event(new UserDeleted($user, $auth_user));
|
||||
|
||||
\Session::flash('flash_success_floating', __('User deleted').': '.$user->getFullName());
|
||||
|
||||
$response['status'] = 'success';
|
||||
@@ -529,7 +592,7 @@ class UsersController extends Controller
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occured';
|
||||
$response['msg'] = 'Unknown error occurred';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
|
||||
@@ -19,7 +19,6 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
// todo: These two may need to be moved into 'web' not to be called in 'api'.
|
||||
\App\Http\Middleware\ResponseHeaders::class,
|
||||
\App\Http\Middleware\TerminateHandler::class,
|
||||
];
|
||||
@@ -34,19 +33,22 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\App\Http\Middleware\TokenAuth::class,
|
||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\HttpsRedirect::class,
|
||||
\App\Http\Middleware\Localize::class,
|
||||
\App\Http\Middleware\LogoutIfDeleted::class,
|
||||
\App\Http\Middleware\FrameGuard::class,
|
||||
\App\Http\Middleware\CustomHandle::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
],
|
||||
// 'api' => [
|
||||
// 'throttle:60,1',
|
||||
// 'bindings',
|
||||
// ],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,9 +16,14 @@ class CustomHandle
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// Enable/disable chat mode
|
||||
if ($request->exists('chat_mode')) {
|
||||
\Helper::setChatMode((int)$request->chat_mode);
|
||||
}
|
||||
|
||||
// Hook.
|
||||
\Eventy::action('middleware.web.custom_handle', $request);
|
||||
|
||||
return $next($request);
|
||||
return \Eventy::filter('middleware.web.custom_handle.response', $next($request), $request, $next);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ class EncryptCookies extends Middleware
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
'in_app'
|
||||
];
|
||||
}
|
||||
|
||||
32
app/Http/Middleware/FrameGuard.php
Normal file
32
app/Http/Middleware/FrameGuard.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class FrameGuard
|
||||
{
|
||||
/**
|
||||
* Handle the given request and get the response.
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
$x_frame_options = config('app.x_frame_options');
|
||||
|
||||
if (false !== $x_frame_options
|
||||
&& $x_frame_options !== 'false'
|
||||
&& $x_frame_options !== '0'
|
||||
) {
|
||||
$value = 'SAMEORIGIN';
|
||||
|
||||
if (is_string($x_frame_options) && preg_match("#(DENY|SAMEORIGIN|ALLOW-FROM)#i", $x_frame_options)) {
|
||||
$value = $x_frame_options;
|
||||
}
|
||||
$response->headers->set('X-Frame-Options', $value, false);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -18,26 +18,31 @@ class HttpsRedirect {
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [
|
||||
Request::HEADER_FORWARDED => 'FORWARDED',
|
||||
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
|
||||
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
|
||||
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
|
||||
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
|
||||
];
|
||||
// protected $headers = [
|
||||
// Request::HEADER_FORWARDED => 'FORWARDED',
|
||||
// Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
|
||||
// Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
|
||||
// Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
|
||||
// Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
|
||||
// ];
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (\Config::get('app.force_https') == 'true') {
|
||||
$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
|
||||
|
||||
if (!$request->secure() && strtolower($_SERVER['HTTPS'] ?? '') != 'on'
|
||||
&& ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') != 'https'
|
||||
&& ($_SERVER['HTTP_CF_VISITOR'] ?? '') != '{"scheme":"https"}'
|
||||
) {
|
||||
if (\Helper::isHttps()) {
|
||||
//$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
|
||||
//!$request->secure()
|
||||
if (!\Helper::isCurrentUrlHttps()) {
|
||||
return redirect()->secure($request->getRequestUri());
|
||||
}
|
||||
}
|
||||
|
||||
// Correct protocol in $_SERVER
|
||||
if (\Helper::isHttps()
|
||||
//&& !$request->secure()
|
||||
&& strtolower($_SERVER['HTTPS'] ?? '') != 'on'
|
||||
) {
|
||||
$_SERVER['HTTPS'] = 'on';
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,9 @@ class Localize
|
||||
// Interface language is set automatically, as locale is stored in .env file.
|
||||
|
||||
// Set user language if user logged in.
|
||||
$user_locale = session('user_locale');
|
||||
$user_locale = \Eventy::filter('locale', session('user_locale'));
|
||||
if ($user_locale) {
|
||||
// app()->setLocale() also dynamically changes config('app.locale'),
|
||||
// so we have to remember current locale in real_locale parameter.
|
||||
\Config::set('app.real_locale', config('app.locale'));
|
||||
app()->setLocale($user_locale);
|
||||
\Helper::setLocale($user_locale);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
||||
30
app/Http/Middleware/LogoutIfDeleted.php
Normal file
30
app/Http/Middleware/LogoutIfDeleted.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
|
||||
class LogoutIfDeleted
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param string|null $guard
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next, $guard = null)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if ($user && ($user->isDeleted() || $user->isDisabled())) {
|
||||
Auth::logout();
|
||||
return Redirect::route('login');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class TerminateHandler
|
||||
|
||||
public function terminate($request, $response)
|
||||
{
|
||||
// Process events which occured
|
||||
// Process events which occurred
|
||||
Subscription::processEvents();
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Http/Middleware/TokenAuth.php
Normal file
34
app/Http/Middleware/TokenAuth.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\User;
|
||||
use Closure;
|
||||
|
||||
class TokenAuth
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// This is needed to restore authentication when app session expires.
|
||||
if (!$request->user() && !empty($request->auth_token) && $request->cookie('in_app')) {
|
||||
try {
|
||||
$user = User::where(\DB::raw('md5(CONCAT(id, created_at, "'.config('app.key').'"))'), $request->auth_token)
|
||||
->first();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '[TokenAuth]');
|
||||
}
|
||||
if (!empty($user)) {
|
||||
\Auth::login($user);
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
45
app/Job.php
45
app/Job.php
@@ -2,12 +2,57 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Thread;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Job extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
public $payload_decoded = null;
|
||||
|
||||
/**
|
||||
* Automatically converted into Carbon dates.
|
||||
*/
|
||||
protected $dates = ['created_at', 'available_at', 'reserved_at'];
|
||||
|
||||
public function getPayloadDecoded()
|
||||
{
|
||||
if ($this->payload_decoded !== null) {
|
||||
return $this->payload_decoded;
|
||||
}
|
||||
|
||||
$this->payload_decoded = json_decode($this->payload, true);
|
||||
|
||||
return $this->payload_decoded;
|
||||
}
|
||||
|
||||
public function getCommand()
|
||||
{
|
||||
return self::getPayloadCommand($this->getPayloadDecoded());
|
||||
}
|
||||
|
||||
public function getCommandLastThread()
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
if ($command && !empty($command->threads)) {
|
||||
return Thread::getLastThread($command->threads);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getPayloadCommand($payload)
|
||||
{
|
||||
if (empty($payload['data']) || empty($payload['data']['command'])) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// If some record has been deleted from DB, there will be an error:
|
||||
// No query results for model [App\Conversation].
|
||||
return unserialize($payload['data']['command']);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
app/Jobs/RestartQueueWorker.php
Normal file
41
app/Jobs/RestartQueueWorker.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Used to stop current queue:work in order to start a new one.
|
||||
* https://github.com/freescout-helpdesk/freescout/issues/2507#issuecomment-1376006247
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RestartQueueWorker implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->delete();
|
||||
// register_shutdown_function() is called on exit(),
|
||||
// so commands mutexes are removed.
|
||||
exit();
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,17 @@ class SendAlert implements ShouldQueue
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $text;
|
||||
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
* fwrite() function in /vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php
|
||||
* in some cases may stuck and continue infinitely. This blocks queue:work and no other jobs are processed.
|
||||
* So we need to set the timeout. On timeout the whole queue:work process is being killed by Laravel.
|
||||
*/
|
||||
public $timeout = 120;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
@@ -43,18 +52,17 @@ class SendAlert implements ShouldQueue
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\MailHelper::setSystemMailDriver();
|
||||
|
||||
$recipients = [];
|
||||
|
||||
$super_admin = User::getSuperAdmin();
|
||||
if ($super_admin) {
|
||||
$recipients[] = $super_admin->email;
|
||||
}
|
||||
$recipients = User::nonDeleted()
|
||||
->where('role', User::ROLE_ADMIN)
|
||||
->where('invite_state', User::INVITE_STATE_ACTIVATED)
|
||||
->pluck('email')
|
||||
->toArray();
|
||||
|
||||
$extra = \MailHelper::sanitizeEmails(\Option::get('alert_recipients'));
|
||||
if ($extra) {
|
||||
$recipients = array_unique(array_merge($recipients, $extra));
|
||||
}
|
||||
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
$exception = null;
|
||||
|
||||
|
||||
@@ -23,6 +23,14 @@ class SendAutoReply implements ShouldQueue
|
||||
|
||||
public $customer;
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
* fwrite() function in /vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php
|
||||
* in some cases may stuck and continue infinitely. This blocks queue:work and no other jobs are processed.
|
||||
* So we need to set the timeout. On timeout the whole queue:work process is being killed by Laravel.
|
||||
*/
|
||||
public $timeout = 120;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
@@ -43,18 +51,28 @@ class SendAutoReply implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Auto reply disabled.
|
||||
if (!empty($this->conversation->meta['ar_off'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($this->mailbox);
|
||||
\App\Misc\Mail::setMailDriver($this->mailbox, null, $this->conversation);
|
||||
|
||||
// Auto reply appears as reply in customer's mailbox
|
||||
$headers['In-Reply-To'] = '<'.$this->thread->message_id.'>';
|
||||
$headers['References'] = '<'.$this->thread->message_id.'>';
|
||||
|
||||
// Create Message-ID for the auto reply
|
||||
$message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_AUTO_REPLY.'-'.$this->thread->id.'-'.md5($this->thread->id).'@'.$this->mailbox->getEmailDomain();
|
||||
$message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_AUTO_REPLY.'-'.$this->thread->id.'-'.\MailHelper::getMessageIdHash($this->thread->id).'@'.$this->mailbox->getEmailDomain();
|
||||
$headers['Message-ID'] = $message_id;
|
||||
|
||||
$customer_email = $this->conversation->customer_email;
|
||||
|
||||
if (!$customer_email) {
|
||||
// When message is received via Chat, customer has no email adddress.
|
||||
return;
|
||||
}
|
||||
$recipients = [$customer_email];
|
||||
$failures = [];
|
||||
$exception = null;
|
||||
|
||||
@@ -24,6 +24,14 @@ class SendEmailReplyError implements ShouldQueue
|
||||
|
||||
public $mailbox;
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
* fwrite() function in /vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php
|
||||
* in some cases may stuck and continue infinitely. This blocks queue:work and no other jobs are processed.
|
||||
* So we need to set the timeout. On timeout the whole queue:work process is being killed by Laravel.
|
||||
*/
|
||||
public $timeout = 120;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
|
||||
@@ -16,15 +16,23 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
// Max retries + 1
|
||||
public $tries = 6;
|
||||
|
||||
public $users;
|
||||
|
||||
public $conversation;
|
||||
|
||||
public $threads;
|
||||
|
||||
// Max retries + 1
|
||||
public $tries = 168; // One per hour
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
* fwrite() function in /vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php
|
||||
* in some cases may stuck and continue infinitely. This blocks queue:work and no other jobs are processed.
|
||||
* So we need to set the timeout. On timeout the whole queue:work process is being killed by Laravel.
|
||||
*/
|
||||
public $timeout = 120;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
@@ -47,31 +55,53 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($mailbox);
|
||||
\App\Misc\Mail::setMailDriver($mailbox, null, $this->conversation);
|
||||
|
||||
// Threads has to be sorted here, if sorted before, they come here in wrong order
|
||||
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
});
|
||||
$this->threads = Thread::sortThreads($this->threads);
|
||||
|
||||
$headers = [];
|
||||
$last_thread = $this->threads->first();
|
||||
|
||||
if (!$last_thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If thread is draft, it means it has been undone
|
||||
if ($last_thread->isDraft()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit conversation history
|
||||
if (config('app.email_user_history') == 'last') {
|
||||
$this->threads = $this->threads->slice(0, 2);
|
||||
}
|
||||
|
||||
if (config('app.email_user_history') == 'none') {
|
||||
$this->threads = $this->threads->slice(0, 1);
|
||||
}
|
||||
|
||||
// All notification for the same conversation has same dummy Message-ID
|
||||
$prev_message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION_IN_REPLY.'-'.$this->conversation->id.'-'.md5($this->conversation->id).'@'.$mailbox->getEmailDomain();
|
||||
$headers['In-Reply-To'] = '<'.$prev_message_id.'>';
|
||||
$headers['References'] = '<'.$prev_message_id.'>';
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2488
|
||||
$headers['X-Auto-Response-Suppress'] = 'All';
|
||||
|
||||
// We throw an exception if any of the send attempts throws an exception (connection error, etc)
|
||||
$global_exception = null;
|
||||
|
||||
foreach ($this->users as $user) {
|
||||
|
||||
// User can ne deleted from DB.
|
||||
if (!isset($user->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($user->isDeleted()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If for one user sending fails the job is marked as failed and retried after some time.
|
||||
// So we have to check if notification email has already been successfully sent to this user.
|
||||
if ($this->attempts() > 1) {
|
||||
@@ -92,7 +122,10 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
// If this is notification on message from customer, set customer as sender name
|
||||
$from_name = '';
|
||||
if ($last_thread->type == Thread::TYPE_CUSTOMER) {
|
||||
$from_name = $last_thread->customer->getFullName();
|
||||
$from_name = '';
|
||||
if ($last_thread->customer) {
|
||||
$from_name = $last_thread->customer->getFullName(true, true);
|
||||
}
|
||||
if ($from_name) {
|
||||
$from_name = $from_name.' '.__('via').' '.$mailbox->name;
|
||||
}
|
||||
@@ -105,6 +138,9 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
// Set user language
|
||||
app()->setLocale($user->getLocale());
|
||||
|
||||
$headers['X-FreeScout-Mail-Type'] = 'user.notification';
|
||||
$headers = \Eventy::filter('jobs.send_reply_to_customer.headers', $headers, $user, $mailbox, $this->conversation, $this->threads, $from);
|
||||
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
@@ -145,8 +181,15 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
if ($global_exception) {
|
||||
// Retry job with delay.
|
||||
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
|
||||
if ($this->attempts() < $this->tries) {
|
||||
$this->release(3600);
|
||||
// We do not try to resend Bounce messages: https://github.com/freescout-helpdesk/freescout/issues/3156
|
||||
if ($this->attempts() < $this->tries && !$last_thread->isBounce()) {
|
||||
if ($this->attempts() == 1) {
|
||||
// Second attempt after 5 min.
|
||||
$this->release(300);
|
||||
} else {
|
||||
// Others - after 1 hour.
|
||||
$this->release(3600);
|
||||
}
|
||||
|
||||
throw $global_exception;
|
||||
} else {
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Mail\ReplyToCustomer;
|
||||
use App\Customer;
|
||||
use App\SendLog;
|
||||
use App\Thread;
|
||||
use App\Misc\SwiftGetSmtpQueueId;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mail;
|
||||
use Webklex\IMAP\Client;
|
||||
|
||||
class SendReplyToCustomer implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
// Number of retries + 1
|
||||
public $tries = 6;
|
||||
|
||||
public $conversation;
|
||||
|
||||
public $threads;
|
||||
@@ -31,6 +31,17 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
private $message_id = '';
|
||||
private $customer_email = '';
|
||||
|
||||
// Number of retries + 1
|
||||
public $tries = 168; // one per hour
|
||||
|
||||
/**
|
||||
* The number of seconds the job can run before timing out.
|
||||
* fwrite() function in /vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php
|
||||
* in some cases may stuck and continue infinitely. This blocks queue:work and no other jobs are processed.
|
||||
* So we need to set the timeout. On timeout the whole queue:work process is being killed by Laravel.
|
||||
*/
|
||||
public $timeout = 120;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
@@ -50,6 +61,9 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$send_previous_messages = false;
|
||||
$is_forward = false;
|
||||
|
||||
// When forwarding conversation is undone, new conversation is deleted.
|
||||
if (!$this->conversation) {
|
||||
return;
|
||||
@@ -57,17 +71,20 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
|
||||
// Mailbox may be deleted.
|
||||
if (!$mailbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add forwarded conversation replies.
|
||||
if ($this->conversation->threads_count == 1 && count($this->threads) == 1) {
|
||||
$forward_child_thread = $this->threads[0];
|
||||
if ($forward_child_thread->isForwarded() && $forward_child_thread->getForwardParentConversation()) {
|
||||
|
||||
|
||||
// Add replies from original conversation.
|
||||
$forwarded_replies = $forward_child_thread->getForwardParentConversation()->getReplies();
|
||||
$forwarded_replies = $forwarded_replies->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
});
|
||||
$forward_parent_thread = Thread::find($forward_child_thread->getMeta('forward_parent_thread_id'));
|
||||
$forwarded_replies = Thread::sortThreads($forwarded_replies);
|
||||
$forward_parent_thread = Thread::find($forward_child_thread->getMetaFw(Thread::META_FORWARD_PARENT_THREAD_ID));
|
||||
|
||||
if ($forward_parent_thread) {
|
||||
// Remove threads created after forwarding.
|
||||
@@ -77,49 +94,150 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
$this->threads = $this->threads->merge($forwarded_replies);
|
||||
$is_forward = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Threads has to be sorted here, if sorted before, they come here in wrong order
|
||||
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
});
|
||||
$this->threads = Thread::sortThreads($this->threads);
|
||||
|
||||
$new = false;
|
||||
$headers = [];
|
||||
|
||||
$this->last_thread = $this->threads->first();
|
||||
$prev_thread = null;
|
||||
|
||||
if ($this->last_thread === null) {
|
||||
return;
|
||||
}
|
||||
$last_customer_thread = null;
|
||||
|
||||
// If thread is draft, it means it has been undone
|
||||
if ($this->last_thread->isDraft()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
|
||||
// After sending an email we are saving it into "IMAP Folder To Save Outgoing Replies".
|
||||
// This process may stuck or make SendReplyToCustomer job die with
|
||||
// "Allowed memory size of NNN bytes exhausted" error.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3632
|
||||
if ($this->attempts() >= 1 &&
|
||||
($this->last_thread->send_status == SendLog::STATUS_ACCEPTED
|
||||
|| $this->last_thread->isSendStatusSuccess())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($this->threads) == 1) {
|
||||
$new = true;
|
||||
}
|
||||
$i = 0;
|
||||
foreach ($this->threads as $thread) {
|
||||
if ($i == 1) {
|
||||
$prev_thread = $thread;
|
||||
break;
|
||||
if (!$new) {
|
||||
$i = 0;
|
||||
foreach ($this->threads as $thread) {
|
||||
if ($i > 0 && $thread->type == Thread::TYPE_CUSTOMER) {
|
||||
$last_customer_thread = $thread;
|
||||
break;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Get penultimate email Message-Id if reply
|
||||
if (!$new && !empty($prev_thread) && $prev_thread->message_id) {
|
||||
$headers['In-Reply-To'] = '<'.$prev_thread->message_id.'>';
|
||||
$headers['References'] = '<'.$prev_thread->message_id.'>';
|
||||
// In-Reply-To and References headers.
|
||||
$references = '';
|
||||
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
|
||||
|
||||
$headers['In-Reply-To'] = '<'.$last_customer_thread->message_id.'>';
|
||||
//$headers['References'] = '<'.$last_customer_thread->message_id.'>';
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3175
|
||||
$i = 0;
|
||||
$references_array = [];
|
||||
foreach ($this->threads as $thread) {
|
||||
if ($i > 0) {
|
||||
$reference = $thread->getMessageId();
|
||||
if ($reference) {
|
||||
$references_array[] = $reference;
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
if ($references_array) {
|
||||
$references = '<'.implode('> <', array_reverse($references_array)).'>';
|
||||
}
|
||||
if ($references) {
|
||||
$headers['References'] = $references;
|
||||
}
|
||||
}
|
||||
$this->message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->last_thread->id.'-'.md5($this->last_thread->id).'@'.$mailbox->getEmailDomain();
|
||||
|
||||
// Conversation history.
|
||||
$email_conv_history = config('app.email_conv_history');
|
||||
|
||||
$threads_count = count($this->threads);
|
||||
|
||||
$meta_conv_history = $this->last_thread->getMeta(Thread::META_CONVERSATION_HISTORY);
|
||||
if (!empty($meta_conv_history)) {
|
||||
$email_conv_history = $meta_conv_history;
|
||||
}
|
||||
|
||||
if ($is_forward && $email_conv_history == 'global') {
|
||||
$email_conv_history = 'full';
|
||||
}
|
||||
|
||||
if ($is_forward && $email_conv_history == 'none') {
|
||||
$email_conv_history = 'full';
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'full') {
|
||||
$send_previous_messages = true;
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'last') {
|
||||
$send_previous_messages = true;
|
||||
$this->threads = $this->threads->slice(0, 2);
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'none') {
|
||||
$send_previous_messages = false;
|
||||
}
|
||||
|
||||
if (!$is_forward) {
|
||||
$send_previous_messages = \Eventy::filter('jobs.send_reply_to_customer.send_previous_messages', $send_previous_messages, $this->last_thread, $this->threads, $this->conversation, $this->customer);
|
||||
}
|
||||
|
||||
// Remove previous messages.
|
||||
if (!$send_previous_messages) {
|
||||
$this->threads = $this->threads->slice(0, 1);
|
||||
}
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\MailHelper::setMailDriver($mailbox, $this->last_thread->created_by_user, $this->conversation);
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3330
|
||||
if (!\MailHelper::$smtp_queue_id_plugin_registered) {
|
||||
\Mail::getSwiftMailer()->registerPlugin(new SwiftGetSmtpQueueId());
|
||||
\MailHelper::$smtp_queue_id_plugin_registered = true;
|
||||
}
|
||||
|
||||
$this->message_id = $this->last_thread->getMessageId($mailbox);
|
||||
$headers['Message-ID'] = $this->message_id;
|
||||
|
||||
$this->customer_email = $this->conversation->customer_email;
|
||||
|
||||
// For phone conversations we may need to get customer email.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3270
|
||||
if (!$this->customer_email && $this->conversation->isPhone()) {
|
||||
$this->customer_email = $this->conversation->customer->getMainEmail();
|
||||
if (!$this->customer_email) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get customer by email
|
||||
if (!$this->customer) {
|
||||
$this->customer = Customer::getByEmail($this->customer_email);
|
||||
return;
|
||||
}
|
||||
|
||||
$to_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getToArray());
|
||||
$cc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getCcArray());
|
||||
$bcc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getBccArray());
|
||||
|
||||
@@ -127,12 +245,27 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
$cc_array = \App\Misc\Mail::removeEmailFromArray($cc_array, $this->customer_email);
|
||||
$bcc_array = \App\Misc\Mail::removeEmailFromArray($bcc_array, $this->customer_email);
|
||||
|
||||
// Auto Bcc.
|
||||
if ($mailbox->auto_bcc) {
|
||||
$auto_bcc = \MailHelper::sanitizeEmails($mailbox->auto_bcc);
|
||||
if ($auto_bcc) {
|
||||
$bcc_array = array_merge($bcc_array, $auto_bcc);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from BCC emails which are present in CC
|
||||
foreach ($cc_array as $cc_email) {
|
||||
$bcc_array = \App\Misc\Mail::removeEmailFromArray($bcc_array, $cc_email);
|
||||
}
|
||||
|
||||
$this->recipients = array_merge([$this->customer_email], $cc_array, $bcc_array);
|
||||
$this->recipients = array_merge($to_array, $cc_array, $bcc_array);
|
||||
|
||||
$to = [];
|
||||
if (count($to_array) > 1) {
|
||||
$to = $to_array;
|
||||
} else {
|
||||
$to = [['name' => $this->customer->getFullName(), 'email' => $this->customer_email]];
|
||||
}
|
||||
|
||||
// If sending fails, all recipiens fail.
|
||||
// if ($this->attempts() > 1) {
|
||||
@@ -140,37 +273,82 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
// $bcc_array = [];
|
||||
// }
|
||||
|
||||
$subject = $this->conversation->subject;
|
||||
if (!$new && !$is_forward) {
|
||||
$subject = 'Re: '.$subject;
|
||||
}
|
||||
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation, $this->last_thread);
|
||||
$this->threads = \Eventy::filter('email.reply_to_customer.threads', $this->threads, $this->conversation, $mailbox);
|
||||
|
||||
$headers['X-FreeScout-Mail-Type'] = 'customer.message';
|
||||
|
||||
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox, $subject, $threads_count);
|
||||
|
||||
$smtp_queue_id = null;
|
||||
|
||||
try {
|
||||
Mail::to([['name' => $this->customer->getFullName(), 'email' => $this->customer_email]])
|
||||
Mail::to($to)
|
||||
->cc($cc_array)
|
||||
->bcc($bcc_array)
|
||||
->send(new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox));
|
||||
->send($reply_mail);
|
||||
|
||||
$this->last_thread->send_status = SendLog::STATUS_ACCEPTED;
|
||||
$this->last_thread->save();
|
||||
|
||||
$smtp_queue_id = SwiftGetSmtpQueueId::$last_smtp_queue_id;
|
||||
} catch (\Exception $e) {
|
||||
// We come here in case SMTP server unavailable for example
|
||||
activity()
|
||||
->causedBy($this->customer)
|
||||
->withProperties([
|
||||
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
|
||||
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
|
||||
if ($this->attempts() == 1) {
|
||||
activity()
|
||||
->causedBy($this->customer)
|
||||
->withProperties([
|
||||
'error' => $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')',
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
|
||||
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER);
|
||||
}
|
||||
|
||||
// Failures will be saved to send log when retry attempts will finish
|
||||
// Mail::failures() is empty in case of connection error.
|
||||
$this->failures = $this->recipients;
|
||||
|
||||
// Save to send log
|
||||
$this->saveToSendLog($e->getMessage());
|
||||
// Save to send log (only first attempt).
|
||||
if ($this->attempts() == 1) {
|
||||
$this->saveToSendLog($e->getMessage());
|
||||
}
|
||||
|
||||
$error_message = $e->getMessage();
|
||||
|
||||
// Remove stack trace from error message.
|
||||
$error_message = preg_replace('#[\r\n]*" in /.*#s', '"', $error_message);
|
||||
|
||||
// SMTP response code is stored in the exception message:
|
||||
// Expected response code 235 but got code "535", with message...
|
||||
preg_match('#but got code "(\d+)",#', $error_message, $response_m);
|
||||
$response_code = (int)($response_m[1] ?? 0);
|
||||
|
||||
// Retry job with delay.
|
||||
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
|
||||
if ($this->attempts() < $this->tries) {
|
||||
$this->release(3600);
|
||||
if ($this->attempts() < $this->tries && !preg_match("/".config("app.no_retry_mail_errors")."/i", $error_message)) {
|
||||
if ($this->attempts() == 1) {
|
||||
// Second attempt after 5 min.
|
||||
$this->release(300);
|
||||
} else {
|
||||
// Others - after 1 hour.
|
||||
$this->release(3600);
|
||||
}
|
||||
|
||||
// If an email has not been sent after 1 hour - show an error message to support agent.
|
||||
if ($this->attempts() >= 3 || $response_code >= 500) {
|
||||
$this->last_thread->send_status = SendLog::STATUS_SEND_INTERMEDIATE_ERROR;
|
||||
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
|
||||
$this->last_thread->save();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
} else {
|
||||
$this->last_thread->send_status = SendLog::STATUS_SEND_ERROR;
|
||||
$this->last_thread->updateSendStatusData(['msg' => $e->getMessage()]);
|
||||
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
|
||||
$this->last_thread->save();
|
||||
|
||||
// This executes $this->failed().
|
||||
@@ -180,6 +358,146 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
SwiftGetSmtpQueueId::$last_smtp_queue_id = null;
|
||||
|
||||
// Clean error message if email finally has been sent.
|
||||
if ($this->last_thread->send_status == SendLog::STATUS_SEND_ERROR) {
|
||||
$this->last_thread->send_status = null;
|
||||
$this->last_thread->updateSendStatusData(['msg' => '']);
|
||||
$this->last_thread->save();
|
||||
}
|
||||
|
||||
$imap_sent_folder = $mailbox->imap_sent_folder;
|
||||
if ($imap_sent_folder) {
|
||||
try {
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
$client->connect();
|
||||
|
||||
$mail_from = $mailbox->getMailFrom($this->last_thread->created_by_user ?? null, $this->conversation);
|
||||
|
||||
if (!empty($mail_from['name'])) {
|
||||
$envelope['from'] = '"'.$mail_from['name'].'" <'.$mail_from['address'].'>';
|
||||
} else {
|
||||
$envelope['from'] = $mail_from['address'];
|
||||
}
|
||||
if (is_array($to) && !empty($to[0]) && !empty($to[0]['name']) && !empty($to[0]['email'])) {
|
||||
$envelope['to'] = '"'.$to[0]['name'].'" <'.$to[0]['email'].'>';
|
||||
} else {
|
||||
$envelope['to'] = $this->customer_email;
|
||||
}
|
||||
$envelope['subject'] = $subject;
|
||||
$envelope['date'] = now()->toRfc2822String();
|
||||
$envelope['message_id'] = $this->message_id;
|
||||
|
||||
// CC.
|
||||
if (count($cc_array)) {
|
||||
$envelope['cc'] = implode(',', $cc_array);
|
||||
}
|
||||
|
||||
// Get penultimate email Message-Id if reply
|
||||
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
|
||||
$envelope['custom_headers'] = [
|
||||
'In-Reply-To: <'.$last_customer_thread->message_id.'>',
|
||||
'References: '.$references,
|
||||
];
|
||||
}
|
||||
// Remove new lines to avoid "imap_mail_compose(): header injection attempt in subject".
|
||||
foreach ($envelope as $i => $envelope_value) {
|
||||
$envelope[$i] = preg_replace("/[\r\n]/", '', $envelope_value);
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
|
||||
// Multipart flag.
|
||||
if ($this->last_thread->has_attachments) {
|
||||
$multipart = [];
|
||||
$multipart["type"] = TYPEMULTIPART;
|
||||
$multipart["subtype"] = "mixed";
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3934
|
||||
//$multipart["subtype"] = "alternative";
|
||||
$parts[] = $multipart;
|
||||
}
|
||||
|
||||
// Body.
|
||||
$part_body['type'] = TYPETEXT;
|
||||
$part_body['subtype'] = 'html';
|
||||
$part_body['contents.data'] = $reply_mail->render();
|
||||
$part_body['charset'] = 'utf-8';
|
||||
|
||||
$parts[] = $part_body;
|
||||
|
||||
// Add attachments.
|
||||
if ($this->last_thread->has_attachments) {
|
||||
|
||||
foreach ($this->last_thread->attachments as $attachment) {
|
||||
|
||||
if ($attachment->embedded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($attachment->fileExists()) {
|
||||
$part = [];
|
||||
$part["type"] = 'APPLICATION';
|
||||
$part["encoding"] = ENCBASE64;
|
||||
$part["subtype"] = "octet-stream";
|
||||
$part["description"] = $attachment->file_name;
|
||||
$part['disposition.type'] = 'attachment';
|
||||
$part['disposition'] = array('filename' => $attachment->file_name);
|
||||
$part['type.parameters'] = array('name' => $attachment->file_name);
|
||||
$part["description"] = '';
|
||||
$part["contents.data"] = base64_encode($attachment->getFileContents());
|
||||
|
||||
$parts[] = $part;
|
||||
} else {
|
||||
\Log::error('[IMAP Folder To Save Outgoing Replies] Thread: '.$this->last_thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3502
|
||||
$imap_sent_folder = mb_convert_encoding($imap_sent_folder, "UTF7-IMAP","UTF-8");
|
||||
|
||||
// https://github.com/Webklex/php-imap/issues/380
|
||||
if (method_exists($client, 'getFolderByPath')) {
|
||||
$folder = $client->getFolderByPath($imap_sent_folder);
|
||||
} else {
|
||||
$folder = $client->getFolder($imap_sent_folder);
|
||||
}
|
||||
// Get folder method does not work if sent folder has spaces.
|
||||
if ($folder) {
|
||||
try {
|
||||
$save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts, $bcc_array);
|
||||
|
||||
// Sometimes emails with attachments by some reason are not saved.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2749
|
||||
if (!$save_result) {
|
||||
// Save without attachments.
|
||||
$save_result = $this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array);
|
||||
if (!$save_result) {
|
||||
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Just log error and continue.
|
||||
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: ');
|
||||
}
|
||||
} else {
|
||||
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Just log error and continue.
|
||||
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
|
||||
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Just log error and continue.
|
||||
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not get mailbox IMAP folder: '.$imap_sent_folder);
|
||||
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
|
||||
}
|
||||
}
|
||||
|
||||
// In message_id we are storing Message-ID of the incoming email which created the thread
|
||||
// Outcoming message_id can be generated for each thread by thread->id
|
||||
// $this->last_thread->message_id = $message_id;
|
||||
@@ -189,7 +507,35 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
$this->failures = Mail::failures();
|
||||
|
||||
// Save to send log
|
||||
$this->saveToSendLog();
|
||||
$this->saveToSendLog('', $smtp_queue_id);
|
||||
}
|
||||
|
||||
public function getImapSaveErrorPrefix($mailbox)
|
||||
{
|
||||
return '['.$mailbox->name.' » Connection Settings » Fetching Emails » IMAP Folder To Save Outgoing Replies] ';
|
||||
}
|
||||
|
||||
// Save an email to IMAP folder.
|
||||
public function saveEmailToFolder($client, $folder, $envelope, $parts, $bcc = [])
|
||||
{
|
||||
$envelope_str = imap_mail_compose($envelope, $parts);
|
||||
|
||||
// Add BCC.
|
||||
// https://stackoverflow.com/questions/47353938/php-imap-append-with-bcc
|
||||
if (!empty($bcc)) {
|
||||
// There will be a "To:" parameter for sure.
|
||||
$to_pos = strpos($envelope_str , "To:");
|
||||
if ($to_pos !== false) {
|
||||
$bcc_str = "Bcc: " . implode(',', $bcc) . "\r\n";
|
||||
$envelope_str = substr_replace($envelope_str , $bcc_str, $to_pos, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (get_class($client) == 'Webklex\PHPIMAP\Client') {
|
||||
return $folder->appendMessage($envelope_str, ['\Seen'], now()->format('d-M-Y H:i:s O'));
|
||||
} else {
|
||||
return $folder->appendMessage($envelope_str, '\Seen', now()->format('d-M-Y H:i:s O'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +564,7 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
/**
|
||||
* Save emails to send log.
|
||||
*/
|
||||
public function saveToSendLog($error_message = '')
|
||||
public function saveToSendLog($error_message = '', $smtp_queue_id = '')
|
||||
{
|
||||
foreach ($this->recipients as $recipient) {
|
||||
if (in_array($recipient, $this->failures)) {
|
||||
@@ -233,7 +579,7 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
} else {
|
||||
$customer_id = null;
|
||||
}
|
||||
SendLog::log($this->last_thread->id, $this->message_id, $recipient, SendLog::MAIL_TYPE_EMAIL_TO_CUSTOMER, $status, $customer_id, null, $status_message);
|
||||
SendLog::log($this->last_thread->id, $this->message_id, $recipient, SendLog::MAIL_TYPE_EMAIL_TO_CUSTOMER, $status, $customer_id, null, $status_message, $smtp_queue_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
app/Jobs/UpdateFolderCounters.php
Normal file
42
app/Jobs/UpdateFolderCounters.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Conversation;
|
||||
use App\ConversationFolder;
|
||||
use App\Folder;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class UpdateFolderCounters implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Folder
|
||||
*/
|
||||
private $folder;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Folder $folder)
|
||||
{
|
||||
$this->folder = $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->folder->updateCountersNow();
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ class LogLockout
|
||||
public function handle(Lockout $event)
|
||||
{
|
||||
activity()
|
||||
->causedBy($event->user)
|
||||
->withProperties(['ip' => app('request')->ip()])
|
||||
//->causedBy($event->user)
|
||||
->withProperties(['ip' => app('request')->ip(), 'email' => $event->request->email])
|
||||
->useLog(\App\ActivityLog::NAME_USER)
|
||||
->log(\App\ActivityLog::DESCRIPTION_USER_LOCKED);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class LogUserDeletion
|
||||
public function handle(UserDeleted $event)
|
||||
{
|
||||
activity()
|
||||
->causedBy($event->user)
|
||||
->causedBy($event->by_user)
|
||||
->withProperties(['deleted_user' => $event->deleted_user->getFullName().' ['.$event->deleted_user->id.']'])
|
||||
->useLog(\App\ActivityLog::NAME_USER)
|
||||
->log(\App\ActivityLog::DESCRIPTION_USER_DELETED);
|
||||
|
||||
25
app/Listeners/ProcessSwiftMessage.php
Normal file
25
app/Listeners/ProcessSwiftMessage.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
class ProcessSwiftMessage
|
||||
{
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
return \Eventy::filter('mail.process_swift_message', true, $event->message);
|
||||
}
|
||||
}
|
||||
30
app/Listeners/RefreshConversations.php
Normal file
30
app/Listeners/RefreshConversations.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Conversation;
|
||||
|
||||
class RefreshConversations
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param $event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
Conversation::refreshConversations($event->conversation, $event->thread ?? $event->last_thread);
|
||||
}
|
||||
}
|
||||
30
app/Listeners/RestartSwiftMailer.php
Normal file
30
app/Listeners/RestartSwiftMailer.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
class RestartSwiftMailer
|
||||
{
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
// Destroy Swift mailer to make it clean temp files.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2949
|
||||
// https://github.com/swiftmailer/swiftmailer/issues/1287#issuecomment-833505298
|
||||
\App::forgetInstance('mailer');
|
||||
\App::forgetInstance('swift.mailer');
|
||||
\App::forgetInstance('swift.transport');
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\SendLog;
|
||||
|
||||
class SendAutoReply
|
||||
{
|
||||
const CHECK_PERIOD = 180; // min
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
@@ -23,7 +27,11 @@ class SendAutoReply
|
||||
{
|
||||
$conversation = $event->conversation;
|
||||
|
||||
if (/*!$conversation->imported &&*/ $conversation->mailbox->auto_reply_enabled) {
|
||||
// no_autoreply meta value is checked in the SendAutoReply job.
|
||||
|
||||
if (!$conversation->imported
|
||||
&& $conversation->mailbox->auto_reply_enabled
|
||||
) {
|
||||
$thread = $conversation->threads()->first();
|
||||
|
||||
// Do not send auto reply to auto responders.
|
||||
@@ -35,20 +43,52 @@ class SendAutoReply
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not send auto replies to spam messages.
|
||||
if ($conversation->status == Conversation::STATUS_SPAM) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\Eventy::filter('autoreply.should_send', true, $conversation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We can not send auto reply to incoming bounce messages, as it will lead to the infinite loop:
|
||||
// application will be sending auto replies and mail server will be sending bounce messages to auto replies.
|
||||
// Bounce detection can not be 100% reliable.
|
||||
// So to prevent infinite loop, we are checking number of auto replies sent to the customer recently.
|
||||
$created_at = \Illuminate\Support\Carbon::now()->subMinutes(10);
|
||||
$created_at = \Illuminate\Support\Carbon::now()->subMinutes(self::CHECK_PERIOD);
|
||||
|
||||
$auto_replies_sent = SendLog::where('customer_id', $conversation->customer_id)
|
||||
->where('mail_type', SendLog::MAIL_TYPE_AUTO_REPLY)
|
||||
->where('created_at', '>', $created_at)
|
||||
->count();
|
||||
|
||||
if ($auto_replies_sent >= 3) {
|
||||
if ($auto_replies_sent >= 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($auto_replies_sent >= 2) {
|
||||
// Find conversations from this customer
|
||||
$prev_conversations = Conversation::select('subject', 'id')
|
||||
->where('customer_id', $conversation->customer_id)
|
||||
->where('created_at', '>', $created_at)
|
||||
->get();
|
||||
|
||||
foreach ($prev_conversations as $prev_conv) {
|
||||
if ($prev_conv->subject == $conversation->subject && $prev_conv->id != $conversation->id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not send autoreplies to own mailboxes.
|
||||
if ($conversation->customer_email) {
|
||||
$is_internal_email = Mailbox::where('email', $conversation->customer_email)->exists();
|
||||
if ($is_internal_email) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 24h limit has been disabled: https://github.com/freescout-helpdesk/freescout/pull/95
|
||||
// Send auto reply once in 24h
|
||||
/*$created_at = \Illuminate\Support\Carbon::now()->subDays(1);
|
||||
|
||||
@@ -62,11 +62,12 @@ class SendNotificationToUsers
|
||||
if (empty($event->conversation) || !$event_type) {
|
||||
return;
|
||||
}
|
||||
$conversation = $event->conversation;
|
||||
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
//if (!$conversation->imported) {
|
||||
// Ignore imported threads.
|
||||
if (!empty($event->thread) && $event->thread->imported) {
|
||||
return;
|
||||
}
|
||||
$conversation = $event->conversation;
|
||||
|
||||
// Using the last argument you can make event to be processed immediately
|
||||
Subscription::registerEvent($event_type, $conversation, $caused_by_user_id/*, true*/);
|
||||
|
||||
@@ -23,16 +23,45 @@ class SendReplyToCustomer
|
||||
{
|
||||
$conversation = $event->conversation;
|
||||
|
||||
// Do not send email if this is a Phone conversation.
|
||||
// Do not send email if this is a Phone conversation and customer has no email.
|
||||
if ($conversation->isPhone()) {
|
||||
if (!$conversation->customer->getMainEmail()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$replies = $conversation->getReplies();
|
||||
|
||||
// Ignore imported messages.
|
||||
if ($replies && $replies->first() && $replies->first()->imported) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove threads added after this event had fired.
|
||||
$thread = $event->last_thread ?? $event->thread ?? null;
|
||||
if ($thread) {
|
||||
foreach ($replies as $i => $reply) {
|
||||
if ($reply->id == $thread->id) {
|
||||
break;
|
||||
} else {
|
||||
$replies->forget($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chat conversation.
|
||||
if ($conversation->isChat()) {
|
||||
\Helper::backgroundAction('chat_conversation.send_reply', [$conversation, $replies, $conversation->customer], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
//if (!$conversation->imported) {
|
||||
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $conversation->getReplies(), $conversation->customer)
|
||||
->delay(now()->addSeconds(Conversation::UNDO_TIMOUT))
|
||||
$delay = \Eventy::filter('conversation.send_reply_to_customer_delay', now()->addSeconds(Conversation::UNDO_TIMOUT), $conversation, $replies);
|
||||
|
||||
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $replies, $conversation->customer)
|
||||
->delay($delay)
|
||||
->onQueue('emails');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ class Alert extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
$subject = '['.\Config::get('app.name').'] ';
|
||||
if (!empty($this->title)) {
|
||||
$subject .= $this->title;
|
||||
|
||||
@@ -44,6 +44,8 @@ class AutoReply extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
$view_params = [];
|
||||
|
||||
// Set headers
|
||||
@@ -56,8 +58,10 @@ class AutoReply extends Mailable
|
||||
];
|
||||
|
||||
// Set variables
|
||||
$subject = \App\Misc\Mail::replaceMailVars($this->mailbox->auto_reply_subject, $data);
|
||||
$view_params['auto_reply_message'] = \App\Misc\Mail::replaceMailVars($this->mailbox->auto_reply_message, $data);
|
||||
$subject = \MailHelper::replaceMailVars($this->mailbox->auto_reply_subject, $data);
|
||||
$view_params['auto_reply_message'] = \MailHelper::replaceMailVars($this->mailbox->auto_reply_message, $data);
|
||||
|
||||
$subject = \Eventy::filter('email.auto_reply.subject', $subject, $this->conversation);
|
||||
|
||||
$message = $this->subject($subject)
|
||||
->view('emails/customer/auto_reply', $view_params)
|
||||
|
||||
@@ -26,6 +26,8 @@ class PasswordChanged extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
$message = $this->subject(__('Password Changed'))
|
||||
->view('emails/user/password_changed')
|
||||
->text('emails/user/password_changed_text');
|
||||
|
||||
@@ -38,17 +38,29 @@ class ReplyToCustomer extends Mailable
|
||||
*/
|
||||
public $mailbox;
|
||||
|
||||
/**
|
||||
* Subject.
|
||||
*/
|
||||
public $subject;
|
||||
|
||||
/**
|
||||
* Number of threads.
|
||||
*/
|
||||
public $threads_count;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($conversation, $threads, $headers, $mailbox)
|
||||
public function __construct($conversation, $threads, $headers, $mailbox, $subject, $threads_count = 1)
|
||||
{
|
||||
$this->conversation = $conversation;
|
||||
$this->threads = $threads;
|
||||
$this->headers = $headers;
|
||||
$this->mailbox = $mailbox;
|
||||
$this->subject = $subject;
|
||||
$this->threads_count = $threads_count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,18 +70,58 @@ class ReplyToCustomer extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
$thread = $this->threads->first();
|
||||
$from_alias = trim($thread->from ?? '');
|
||||
|
||||
// Set Message-ID
|
||||
// Settings via $this->addCustomHeaders does not work
|
||||
$new_headers = $this->headers;
|
||||
if (!empty($new_headers)) {
|
||||
$this->withSwiftMessage(function ($swiftmessage) use ($new_headers) {
|
||||
if (!empty($new_headers['Message-ID'])) {
|
||||
$swiftmessage->setId($new_headers['Message-ID']);
|
||||
if (!empty($new_headers) || $from_alias) {
|
||||
$mailbox = $this->mailbox;
|
||||
$this->withSwiftMessage(function ($swiftmessage) use ($new_headers, $from_alias, $mailbox, $thread) {
|
||||
if (!empty($new_headers)) {
|
||||
if (!empty($new_headers['Message-ID'])) {
|
||||
$swiftmessage->setId($new_headers['Message-ID']);
|
||||
}
|
||||
$headers = $swiftmessage->getHeaders();
|
||||
foreach ($new_headers as $header => $value) {
|
||||
if ($header != 'Message-ID') {
|
||||
$headers->addTextHeader($header, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
$headers = $swiftmessage->getHeaders();
|
||||
foreach ($new_headers as $header => $value) {
|
||||
if ($header != 'Message-ID') {
|
||||
$headers->addTextHeader($header, $value);
|
||||
if (!empty($from_alias)) {
|
||||
$aliases = $mailbox->getAliases();
|
||||
|
||||
// Make sure that the From contains a mailbox alias,
|
||||
// as user thread may have From specified when a user
|
||||
// replies to an email notification.
|
||||
if (array_key_exists($from_alias, $aliases)) {
|
||||
|
||||
$from_alias_name = $aliases[$from_alias] ?? '';
|
||||
|
||||
// Take into account mailbox From Name setting.
|
||||
$mailbox_mail_from = $mailbox->getMailFrom($thread->created_by_user, $thread->conversation);
|
||||
if ($mailbox_mail_from['name'] == $mailbox->name && $from_alias_name) {
|
||||
// Use name from alias.
|
||||
} else {
|
||||
// User name or custom.
|
||||
$from_alias_name = $mailbox_mail_from['name'];
|
||||
}
|
||||
|
||||
$swift_from = $headers->get('From');
|
||||
|
||||
if ($from_alias_name) {
|
||||
$swift_from->setNameAddresses([
|
||||
$from_alias => $from_alias_name
|
||||
]);
|
||||
} else {
|
||||
$swift_from->setAddresses([
|
||||
$from_alias
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,20 +129,19 @@ class ReplyToCustomer extends Mailable
|
||||
});
|
||||
}
|
||||
|
||||
$subject = $this->conversation->subject;
|
||||
if (count($this->threads) > 1) {
|
||||
$subject = 'Re: '.$subject;
|
||||
}
|
||||
|
||||
// from($this->from) Sets only email, name stays empty.
|
||||
// So we set from in Mail::setMailDriver
|
||||
$message = $this->subject($subject)
|
||||
$message = $this->subject($this->subject)
|
||||
->view('emails/customer/reply_fancy')
|
||||
->text('emails/customer/reply_fancy_text');
|
||||
|
||||
if ($this->threads->first()->has_attachments) {
|
||||
foreach ($this->threads->first()->attachments as $attachment) {
|
||||
$message->attach($attachment->getLocalFilePath());
|
||||
if ($thread->has_attachments) {
|
||||
foreach ($thread->attachments as $attachment) {
|
||||
if ($attachment->fileExists()) {
|
||||
$message->attach($attachment->getLocalFilePath());
|
||||
} else {
|
||||
\Log::error('[ReplyToCustomer] Thread: '.$thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,15 @@ class Test extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
$this->withSwiftMessage(function ($swiftmessage) {
|
||||
$headers = $swiftmessage->getHeaders();
|
||||
$headers->addTextHeader('X-FreeScout-Mail-Type', 'test.mailbox');
|
||||
|
||||
return $swiftmessage;
|
||||
});
|
||||
|
||||
$message = $this->subject(__(':app_name Test Email', ['app_name' => \Config::get('app.name')]));
|
||||
if ($this->mailbox) {
|
||||
$message->view('emails/user/test', ['mailbox' => $this->mailbox]);
|
||||
|
||||
@@ -25,6 +25,8 @@ class UserEmailReplyError extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
return $this->subject(__('Unable to process your update'))
|
||||
->view('emails/user/email_reply_error');
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ class UserInvite extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$message = $this->subject(__('Welcome to :company_name', ['company_name' => Option::get('company_name')]))
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
$message = $this->subject(__('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]))
|
||||
->view('emails/user/user_invite')
|
||||
->text('emails/user/user_invite_text');
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ class UserNotification extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\MailHelper::prepareMailable($this);
|
||||
|
||||
// Set Message-ID
|
||||
// Settings via $this->addCustomHeaders does not work
|
||||
$new_headers = $this->headers;
|
||||
|
||||
449
app/Mailbox.php
449
app/Mailbox.php
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Email;
|
||||
use App\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
@@ -9,6 +11,8 @@ use Watson\Rememberable\Rememberable;
|
||||
class Mailbox extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
/**
|
||||
* From Name: name that will appear in the From field when a customer views your email.
|
||||
@@ -23,6 +27,7 @@ class Mailbox extends Model
|
||||
const TICKET_STATUS_ACTIVE = 1;
|
||||
const TICKET_STATUS_PENDING = 2;
|
||||
const TICKET_STATUS_CLOSED = 3;
|
||||
const TICKET_STATUS_KEEP_CURRENT = 0;
|
||||
|
||||
/**
|
||||
* Default Assignee.
|
||||
@@ -30,6 +35,7 @@ class Mailbox extends Model
|
||||
const TICKET_ASSIGNEE_ANYONE = 1;
|
||||
const TICKET_ASSIGNEE_REPLYING_UNASSIGNED = 2;
|
||||
const TICKET_ASSIGNEE_REPLYING = 3;
|
||||
const TICKET_ASSIGNEE_KEEP_CURRENT = 0;
|
||||
|
||||
/**
|
||||
* Email Template.
|
||||
@@ -52,6 +58,9 @@ class Mailbox extends Model
|
||||
const OUT_ENCRYPTION_NONE = 1;
|
||||
const OUT_ENCRYPTION_SSL = 2;
|
||||
const OUT_ENCRYPTION_TLS = 3;
|
||||
// StartTLS is already included in TLS:
|
||||
// allow_self_signed = true
|
||||
// verify_peer = false
|
||||
|
||||
public static $out_encryptions = [
|
||||
self::OUT_ENCRYPTION_NONE => '',
|
||||
@@ -76,11 +85,16 @@ class Mailbox extends Model
|
||||
const IN_ENCRYPTION_NONE = 1;
|
||||
const IN_ENCRYPTION_SSL = 2;
|
||||
const IN_ENCRYPTION_TLS = 3;
|
||||
// For Webklex/laravel-imap this option is the same as 'none'.
|
||||
// For Webklex/php-imap it works:
|
||||
// https://github.com/Webklex/php-imap/pull/180
|
||||
const IN_ENCRYPTION_STARTTLS = 4;
|
||||
|
||||
public static $in_encryptions = [
|
||||
self::IN_ENCRYPTION_NONE => '',
|
||||
self::IN_ENCRYPTION_SSL => 'ssl',
|
||||
self::IN_ENCRYPTION_TLS => 'tls',
|
||||
self::IN_ENCRYPTION_STARTTLS => 'starttls',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -89,6 +103,29 @@ class Mailbox extends Model
|
||||
const RATINGS_PLACEMENT_ABOVE = 1;
|
||||
const RATINGS_PLACEMENT_BELOW = 2;
|
||||
|
||||
/**
|
||||
* Access permissions.
|
||||
*/
|
||||
const ACCESS_PERM_EDIT = 'edit';
|
||||
const ACCESS_PERM_PERMISSIONS = 'perm';
|
||||
const ACCESS_PERM_AUTO_REPLIES = 'auto';
|
||||
const ACCESS_PERM_SIGNATURE = 'sig';
|
||||
const ACCESS_PERM_ASSIGNED = 'asg';
|
||||
|
||||
public static $access_permissions = [
|
||||
self::ACCESS_PERM_EDIT,
|
||||
self::ACCESS_PERM_PERMISSIONS,
|
||||
self::ACCESS_PERM_AUTO_REPLIES,
|
||||
self::ACCESS_PERM_SIGNATURE,
|
||||
];
|
||||
|
||||
public static $access_routes = [
|
||||
self::ACCESS_PERM_EDIT => 'mailboxes.update',
|
||||
self::ACCESS_PERM_PERMISSIONS => 'mailboxes.permissions',
|
||||
self::ACCESS_PERM_AUTO_REPLIES => 'mailboxes.auto_reply',
|
||||
self::ACCESS_PERM_SIGNATURE => 'mailboxes.update',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default signature set when mailbox created.
|
||||
*/
|
||||
@@ -102,20 +139,24 @@ class Mailbox extends Model
|
||||
'signature' => self::DEFAULT_SIGNATURE,
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Attributes fillable using fill() method.
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['name', 'slug', 'email', 'aliases', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'template', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text'];
|
||||
protected $fillable = ['name', 'email', 'aliases', 'aliases_reply', 'auto_bcc', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'template', 'before_reply', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text', 'imap_sent_folder'];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
self::created(function (Mailbox $model) {
|
||||
$model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
|
||||
});
|
||||
// self::created(function (Mailbox $model) {
|
||||
// $model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +164,11 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function setInPasswordAttribute($value)
|
||||
{
|
||||
$this->attributes['in_password'] = encrypt($value);
|
||||
if ($value != '') {
|
||||
$this->attributes['in_password'] = encrypt($value);
|
||||
} else {
|
||||
$this->attributes['in_password'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +184,36 @@ class Mailbox extends Model
|
||||
return decrypt($value);
|
||||
} catch (\Exception $e) {
|
||||
// do nothing if decrypt wasn't succefull
|
||||
return false;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically encrypt password on save.
|
||||
*/
|
||||
public function setOutPasswordAttribute($value)
|
||||
{
|
||||
if ($value != '') {
|
||||
$this->attributes['out_password'] = encrypt($value);
|
||||
} else {
|
||||
$this->attributes['out_password'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically decrypt password on read.
|
||||
*/
|
||||
public function getOutPasswordAttribute($value)
|
||||
{
|
||||
if (!$value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return decrypt($value);
|
||||
} catch (\Exception $e) {
|
||||
// do nothing if decrypt wasn't succefull
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +222,24 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany('App\User')->as('settings')->withPivot('after_send');
|
||||
return $this->belongsToMany('App\User');
|
||||
}
|
||||
|
||||
public function usersWithSettings()
|
||||
{
|
||||
return $this->belongsToMany('App\User')->as('settings')
|
||||
->withPivot('after_send')
|
||||
->withPivot('hide')
|
||||
->withPivot('mute')
|
||||
->withPivot('access');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users having access to the mailbox.
|
||||
*/
|
||||
public function users_cached()
|
||||
{
|
||||
return $this->users()->rememberForever();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,11 +295,11 @@ class Mailbox extends Model
|
||||
continue;
|
||||
}
|
||||
foreach ($folder_types as $type) {
|
||||
$folder = new Folder();
|
||||
$folder->mailbox_id = $mailbox_id;
|
||||
$folder->user_id = $user_id;
|
||||
$folder->type = $type;
|
||||
$folder->save();
|
||||
Folder::create([
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,6 +336,11 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getMainFolders()
|
||||
{
|
||||
$main_folders = \Eventy::filter('mailbox.main_folders', [], $this);
|
||||
if ($main_folders) {
|
||||
return $main_folders;
|
||||
}
|
||||
|
||||
return $this->folders()
|
||||
->where(function ($query) {
|
||||
$query->whereIn('type', [Folder::TYPE_UNASSIGNED, Folder::TYPE_ASSIGNED, Folder::TYPE_DRAFTS])
|
||||
@@ -276,9 +372,9 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getAssesibleFolders()
|
||||
{
|
||||
return $this->folders()
|
||||
$folders = $this->folders()
|
||||
->where(function ($query) {
|
||||
$query->whereIn('type', Folder::$public_types)
|
||||
$query->whereIn('type', \Eventy::filter('mailbox.folders.public_types', Folder::$public_types))
|
||||
->orWhere(function ($query2) {
|
||||
$query2->whereIn('type', Folder::$personal_types);
|
||||
$query2->where(['user_id' => auth()->user()->id]);
|
||||
@@ -286,6 +382,8 @@ class Mailbox extends Model
|
||||
})
|
||||
->orderBy('type')
|
||||
->get();
|
||||
|
||||
return \Eventy::filter('mailbox.folders', $folders, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,6 +419,12 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function isInActive()
|
||||
{
|
||||
$in_active = \Eventy::filter('mailbox.in_active', null, $this);
|
||||
|
||||
if (is_bool($in_active)) {
|
||||
return $in_active;
|
||||
}
|
||||
|
||||
if ($this->in_protocol && $this->in_server && $this->in_port && $this->in_username && $this->in_password) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -347,19 +451,68 @@ class Mailbox extends Model
|
||||
/**
|
||||
* Get users who have access to the mailbox.
|
||||
*/
|
||||
public function usersHavingAccess($cache = false, $fields = 'users.*')
|
||||
public function usersHavingAccess($cache = false, $fields = 'users.*', $sort = true)
|
||||
{
|
||||
$admins = User::where('role', User::ROLE_ADMIN)->select($fields)->remember(\App\Misc\Helper::cacheTime($cache))->get();
|
||||
$admins = User::where('role', User::ROLE_ADMIN)->select($fields)->remember(\Helper::cacheTime($cache))->get();
|
||||
|
||||
$users = $this->users()->select($fields)->rememberForever()->get()->merge($admins)->unique();
|
||||
$users = $this->users()->select($fields)->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
|
||||
|
||||
// Exclude deleted users (better to do it in PHP).
|
||||
foreach ($users as $i => $user) {
|
||||
if ($user->isDeleted()) {
|
||||
if (!$user->isActive()) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by full name
|
||||
if ($sort) {
|
||||
$users = User::sortUsers($users);
|
||||
}
|
||||
|
||||
$users = \Eventy::filter('mailbox.users_having_access', $users, $this, $cache, $fields, $sort);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function usersAssignable($cache = true, $exclude_hidden = true)
|
||||
{
|
||||
// Exclude hidden admins.
|
||||
$mailbox_id = $this->id;
|
||||
$admins = User::select(['users.*', 'mailbox_user.hide'])
|
||||
->leftJoin('mailbox_user', function ($join) use ($mailbox_id) {
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
$join->where('mailbox_user.mailbox_id', $mailbox_id);
|
||||
})
|
||||
->where('role', User::ROLE_ADMIN)
|
||||
->remember(\Helper::cacheTime($cache))
|
||||
->get();
|
||||
|
||||
$users = $this->users()->select(['users.*', 'mailbox_user.hide'])
|
||||
->remember(\Helper::cacheTime($cache))
|
||||
->get()
|
||||
->merge($admins)
|
||||
->unique();
|
||||
|
||||
if ($exclude_hidden) {
|
||||
foreach ($users as $i => $user) {
|
||||
if (!empty($user->hide)) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude deleted users (better to do it in PHP).
|
||||
foreach ($users as $i => $user) {
|
||||
if (!$user->isActive()) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by full name
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
$users = \Eventy::filter('mailbox.users_assignable', $users, $this, $cache);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
@@ -384,9 +537,16 @@ class Mailbox extends Model
|
||||
public function userHasAccess($user_id, $user = null)
|
||||
{
|
||||
if (!$user) {
|
||||
$user = User::find($user_id);
|
||||
if ($user_id instanceof \App\User) {
|
||||
$user = $user_id;
|
||||
} else {
|
||||
$user = User::find($user_id);
|
||||
}
|
||||
}
|
||||
if ($user && $user->isAdmin()) {
|
||||
$filter = \Eventy::filter('mailbox.user_has_access', -1, $this, $user);
|
||||
if ($filter != -1) {
|
||||
return (bool)$filter;
|
||||
} elseif ($user && $user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
return (bool) $this->users()->where('users.id', $user_id)->count();
|
||||
@@ -397,21 +557,31 @@ class Mailbox extends Model
|
||||
* Get From array for the Mail function.
|
||||
*
|
||||
* @param App\User $from_user
|
||||
* @param App\Conversation $conversation
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMailFrom($from_user = null)
|
||||
public function getMailFrom($from_user = null, $conversation = null)
|
||||
{
|
||||
// Mailbox name by default
|
||||
$name = $this->name;
|
||||
|
||||
if ($this->from_name == self::FROM_NAME_CUSTOM && $this->from_name_custom) {
|
||||
$name = $this->from_name_custom;
|
||||
$data = [
|
||||
'mailbox' => $this,
|
||||
'mailbox_from_name' => '', // To avoid recursion.
|
||||
'conversation' => $conversation,
|
||||
'user' => $from_user ?: auth()->user(),
|
||||
];
|
||||
$name = \MailHelper::replaceMailVars($this->from_name_custom, $data, false, true);
|
||||
} elseif ($this->from_name == self::FROM_NAME_USER && $from_user) {
|
||||
$name = $from_user->getFullName();
|
||||
}
|
||||
|
||||
return ['address' => $this->email, 'name' => $name];
|
||||
return [
|
||||
'address' => \Eventy::filter('mailbox.get_mail_from_address', $this->email, $from_user, $conversation),
|
||||
'name' => \Eventy::filter('mailbox.get_mail_from_name', $name, $from_user, $conversation)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -471,7 +641,32 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getInProtocolName()
|
||||
{
|
||||
return self::$in_protocols[$this->in_protocol];
|
||||
return $this->getInProtocols()[$this->in_protocol] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incoming protocol display name for the UI.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getInProtocolDisplayNames()
|
||||
{
|
||||
$display_names = self::$in_protocols;
|
||||
|
||||
$display_names[self::IN_PROTOCOL_IMAP] = 'IMAP';
|
||||
$display_names[self::IN_PROTOCOL_POP3] = 'POP3';
|
||||
|
||||
return \Eventy::filter('mailbox.in_protocols.display_names', $display_names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available incoming protocols.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getInProtocols()
|
||||
{
|
||||
return \Eventy::filter('mailbox.in_protocols', self::$in_protocols);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,19 +674,40 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getUserSettings($user_id)
|
||||
{
|
||||
$mailbox_user = $this->users()->where('users.id', $user_id)->first();
|
||||
$mailbox_user = $this->usersWithSettings()->where('users.id', $user_id)->first();
|
||||
if ($mailbox_user) {
|
||||
return $mailbox_user->settings;
|
||||
} else {
|
||||
// Admin may have no record in mailbox_user table
|
||||
// Create dummy object with default parameters
|
||||
$settings = new \StdClass();
|
||||
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
|
||||
|
||||
return $settings;
|
||||
return self::getDummySettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create dummy object with default parameters
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getDummySettings()
|
||||
{
|
||||
$settings = new \StdClass();
|
||||
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
|
||||
$settings->hide = false;
|
||||
$settings->mute = false;
|
||||
$settings->access = [];
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function fetchUserSettings($user_id)
|
||||
{
|
||||
$settings = $this->getUserSettings($user_id);
|
||||
|
||||
$this->after_send = $settings->after_send;
|
||||
$this->hide = $settings->hide;
|
||||
$this->mute = $settings->mute;
|
||||
$this->access = $settings->access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main email and aliases.
|
||||
*
|
||||
@@ -504,6 +720,9 @@ class Mailbox extends Model
|
||||
if ($this->aliases) {
|
||||
$aliases = explode(',', $this->aliases);
|
||||
foreach ($aliases as $alias) {
|
||||
$alias = trim($alias);
|
||||
$alias = preg_replace("#\(.*#", '', $alias);
|
||||
|
||||
$alias = Email::sanitizeEmail($alias);
|
||||
if ($alias) {
|
||||
$emails[] = $alias;
|
||||
@@ -514,6 +733,42 @@ class Mailbox extends Model
|
||||
return $emails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mailbox aliases as an associative array.
|
||||
*/
|
||||
public function getAliases($include_mailbox_email = true, $check_aliases_reply = false)
|
||||
{
|
||||
if ($check_aliases_reply && !$this->aliases_reply) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($include_mailbox_email) {
|
||||
$emails = [$this->email => $this->name];
|
||||
} else {
|
||||
$emails = [];
|
||||
}
|
||||
|
||||
if ($this->aliases) {
|
||||
$aliases = explode(',', $this->aliases);
|
||||
foreach ($aliases as $alias) {
|
||||
$name = '';
|
||||
$alias = trim($alias);
|
||||
preg_match("#[^\(]+\((.*)\)#", $alias, $m);
|
||||
if (!empty($m[1])) {
|
||||
$name = $m[1];
|
||||
$alias = preg_replace("#\(.*#", '', $alias);
|
||||
}
|
||||
|
||||
$alias = Email::sanitizeEmail($alias);
|
||||
if ($alias) {
|
||||
$emails[$alias] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $emails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove mailbox email and aliases from the list of emails.
|
||||
*
|
||||
@@ -564,7 +819,7 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return route('mailboxes.view', ['id' => $this->id]);
|
||||
return \Eventy::filter('mailbox.url', route('mailboxes.view', ['id' => $this->id]), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -596,6 +851,136 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getInImapFolders()
|
||||
{
|
||||
return \Helper::jsonToArray($this->in_imap_folders);
|
||||
$in_imap_folders = \Helper::jsonToArray($this->in_imap_folders);
|
||||
if (count($in_imap_folders)) {
|
||||
return $in_imap_folders;
|
||||
} else {
|
||||
return ["INBOX"];
|
||||
}
|
||||
}
|
||||
|
||||
public function outPasswordSafe()
|
||||
{
|
||||
return \Helper::safePassword($this->out_password);
|
||||
}
|
||||
|
||||
public function inPasswordSafe()
|
||||
{
|
||||
return \Helper::safePassword($this->in_password);
|
||||
}
|
||||
|
||||
public function getReplySeparator()
|
||||
{
|
||||
return $this->before_reply ?: \MailHelper::REPLY_SEPARATOR_TEXT;
|
||||
}
|
||||
|
||||
public static function findOrFailWithSettings($id, $user_id)
|
||||
{
|
||||
return Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
|
||||
->where('mailboxes.id', $id)
|
||||
->leftJoin('mailbox_user', function ($join) use ($user_id) {
|
||||
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
|
||||
$join->where('mailbox_user.user_id', $user_id);
|
||||
})->firstOrFail();
|
||||
}
|
||||
|
||||
/*public static function getUserSettings($mailbox_id, $user_id)
|
||||
{
|
||||
return MailboxUser::where('mailbox_id', $mailbox_id)
|
||||
->where('user_id', $user_id)
|
||||
->first();
|
||||
}*/
|
||||
|
||||
public static function getAccessPermissionName($perm)
|
||||
{
|
||||
$access_permissions = [
|
||||
self::ACCESS_PERM_EDIT => __('Edit Mailbox'),
|
||||
self::ACCESS_PERM_PERMISSIONS => __('Permissions'),
|
||||
self::ACCESS_PERM_AUTO_REPLIES => __('Auto Replies'),
|
||||
self::ACCESS_PERM_SIGNATURE => __('Email Signature'),
|
||||
];
|
||||
$access_permissions = \Eventy::filter('mailbox.access_permissions_list', $access_permissions);
|
||||
|
||||
return $access_permissions[$perm] ?? '';
|
||||
}
|
||||
|
||||
public static function getAccessPermissionRoute($perm)
|
||||
{
|
||||
$route = self::$access_routes[$perm] ?? '';
|
||||
$route = \Eventy::filter('mailbox.access_permissions_route', $route, $perm);
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
public function getMeta($key, $default = null)
|
||||
{
|
||||
if (isset($this->meta[$key])) {
|
||||
return $this->meta[$key];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
public function setMeta($key, $value)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
$meta[$key] = $value;
|
||||
$this->meta = $meta;
|
||||
}
|
||||
|
||||
public function setMetaParam($param, $value, $save = false)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
$meta[$param] = $value;
|
||||
$this->meta = $meta;
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeMetaParam($param, $save = false)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
if (isset($meta[$param])) {
|
||||
unset($meta[$param]);
|
||||
}
|
||||
$this->meta = $meta;
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a user with specified email.
|
||||
*/
|
||||
public static function userEmailExists($email)
|
||||
{
|
||||
$email = Email::sanitizeEmail($email);
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if ($user) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function oauthEnabled()
|
||||
{
|
||||
return !empty($this->meta['oauth']['provider']);
|
||||
}
|
||||
|
||||
public function oauthGetParam($param)
|
||||
{
|
||||
return $this->meta['oauth'][$param] ?? '';
|
||||
}
|
||||
|
||||
public function setEmailAttribute($value)
|
||||
{
|
||||
if ($value) {
|
||||
$this->attributes['email'] = Email::sanitizeEmail($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
|
||||
class MailboxUser extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
// Action after sending a message
|
||||
const AFTER_SEND_STAY = 1;
|
||||
const AFTER_SEND_NEXT = 2;
|
||||
const AFTER_SEND_FOLDER = 3;
|
||||
|
||||
protected $table = 'mailbox_user';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
// Does not work as we receive it via pivot
|
||||
// protected $casts = [
|
||||
// 'access' => 'array',
|
||||
// ];
|
||||
}
|
||||
|
||||
1198
app/Misc/Helper.php
1198
app/Misc/Helper.php
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ class Mail
|
||||
*/
|
||||
const REPLY_SEPARATOR_HTML = 'fsReplyAbove';
|
||||
const REPLY_SEPARATOR_TEXT = '-- Please reply above this line --';
|
||||
const REPLY_SEPARATOR_NOTIFICATION = 'fsNotifReplyAbove';
|
||||
|
||||
/**
|
||||
* Message-ID prefixes for outgoing emails.
|
||||
@@ -39,12 +40,16 @@ class Mail
|
||||
const MAIL_ENCRYPTION_TLS = 'tls';
|
||||
|
||||
const FETCH_SCHEDULE_EVERY_MINUTE = 1;
|
||||
const FETCH_SCHEDULE_EVERY_TWO_MINUTES = 2;
|
||||
const FETCH_SCHEDULE_EVERY_THREE_MINUTES = 3;
|
||||
const FETCH_SCHEDULE_EVERY_FIVE_MINUTES = 5;
|
||||
const FETCH_SCHEDULE_EVERY_TEN_MINUTES = 10;
|
||||
const FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES = 15;
|
||||
const FETCH_SCHEDULE_EVERY_THIRTY_MINUTES = 30;
|
||||
const FETCH_SCHEDULE_HOURLY = 60;
|
||||
|
||||
const OAUTH_PROVIDER_MICROSOFT = 'ms';
|
||||
|
||||
/**
|
||||
* If reply is not extracted properly from the incoming email, add here a new separator.
|
||||
* Order is not important.
|
||||
@@ -56,7 +61,10 @@ class Mail
|
||||
self::REPLY_SEPARATOR_TEXT, // Our plain text separator
|
||||
|
||||
// Email service providers specific separators.
|
||||
// <div class="gmail_quote" style="font-family:sans-serif;">
|
||||
'<div class="gmail_quote">', // Gmail
|
||||
'<div class="gmail_quote" ', // Gmail
|
||||
'<div id="appendonsend"></div>', // Outlook / Live / Hotmail / Microsoft
|
||||
'<div name="quote" ',
|
||||
'yahoo_quoted_', // Yahoo, full: <div id=3D"ydp6h4f5c59yahoo_quoted_2937493705"
|
||||
'------------------ 原始邮件 ------------------', // QQ
|
||||
@@ -65,10 +73,11 @@ class Mail
|
||||
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0in 0in 0in">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
|
||||
|
||||
// General separators.
|
||||
'<blockquote', // General sepator
|
||||
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
|
||||
'<!-- originalMessage -->',
|
||||
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
|
||||
//'---Original---', // QQ separator, wait for emails from QQ and check
|
||||
'--------------- Original Message ---------------',
|
||||
'-------- Αρχικό μήνυμα --------', // Greek
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -76,17 +85,24 @@ class Mail
|
||||
*/
|
||||
public static $last_mail_config_hash = '';
|
||||
|
||||
/**
|
||||
* Used to get SMTP queue id when sending emails to customers.
|
||||
*/
|
||||
public static $smtp_queue_id_plugin_registered = false;
|
||||
|
||||
/**
|
||||
* Configure mail sending parameters.
|
||||
*
|
||||
* @param App\Mailbox $mailbox
|
||||
* @param App\User $user_from
|
||||
* @param App\Conversation $conversation
|
||||
*/
|
||||
public static function setMailDriver($mailbox = null, $user_from = null)
|
||||
public static function setMailDriver($mailbox = null, $user_from = null, $conversation = null)
|
||||
{
|
||||
if ($mailbox) {
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\Config::set('mail.driver', $mailbox->getMailDriverName());
|
||||
\Config::set('mail.from', $mailbox->getMailFrom($user_from));
|
||||
\Config::set('mail.from', $mailbox->getMailFrom($user_from, $conversation));
|
||||
|
||||
// SMTP
|
||||
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
@@ -134,6 +150,8 @@ class Mail
|
||||
// We have to update Mailer facade manually, as it does not happen automatically
|
||||
// and previous instance of app('mailer') is used.
|
||||
\Mail::swap(app('mailer'));
|
||||
|
||||
\Eventy::action('mail.reapply_mail_config');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +176,7 @@ class Mail
|
||||
\Config::set('mail.password', null);
|
||||
} else {
|
||||
\Config::set('mail.username', Option::get('mail_username'));
|
||||
\Config::set('mail.password', Option::get('mail_password'));
|
||||
\Config::set('mail.password', \Helper::decrypt(Option::get('mail_password')));
|
||||
}
|
||||
\Config::set('mail.encryption', Option::get('mail_encryption'));
|
||||
}
|
||||
@@ -169,7 +187,7 @@ class Mail
|
||||
/**
|
||||
* Replace mail vars in the text.
|
||||
*/
|
||||
public static function replaceMailVars($text, $data = [])
|
||||
public static function replaceMailVars($text, $data = [], $escape = false, $remove_non_replaced = false)
|
||||
{
|
||||
// Available variables to insert into email in UI.
|
||||
$vars = [];
|
||||
@@ -182,19 +200,51 @@ class Mail
|
||||
if (!empty($data['mailbox'])) {
|
||||
$vars['{%mailbox.email%}'] = $data['mailbox']->email;
|
||||
$vars['{%mailbox.name%}'] = $data['mailbox']->name;
|
||||
// To avoid recursion.
|
||||
if (isset($data['mailbox_from_name'])) {
|
||||
$vars['{%mailbox.fromName%}'] = $data['mailbox_from_name'];
|
||||
} else {
|
||||
$vars['{%mailbox.fromName%}'] = $data['mailbox']->getMailFrom(!empty($data['user']) ? $data['user'] : null)['name'];
|
||||
}
|
||||
}
|
||||
if (!empty($data['customer'])) {
|
||||
$vars['{%customer.fullName%}'] = $data['customer']->getFullName(true);
|
||||
$vars['{%customer.firstName%}'] = $data['customer']->getFirstName(true);
|
||||
$vars['{%customer.lastName%}'] = $data['customer']->last_name;
|
||||
$vars['{%customer.company%}'] = $data['customer']->company;
|
||||
}
|
||||
if (!empty($data['user'])) {
|
||||
$vars['{%user.fullName%}'] = $data['user']->getFullName();
|
||||
$vars['{%user.firstName%}'] = $data['user']->getFirstName();
|
||||
$vars['{%user.phone%}'] = $data['user']->phone;
|
||||
$vars['{%user.email%}'] = $data['user']->email;
|
||||
$vars['{%user.jobTitle%}'] = $data['user']->job_title;
|
||||
$vars['{%user.lastName%}'] = $data['user']->last_name;
|
||||
$vars['{%user.photoUrl%}'] = $data['user']->getPhotoUrl();
|
||||
}
|
||||
|
||||
return strtr($text, $vars);
|
||||
$vars = \Eventy::filter('mail_vars.replace', $vars, $data);
|
||||
|
||||
if ($escape) {
|
||||
foreach ($vars as $i => $var) {
|
||||
$vars[$i] = htmlspecialchars($var ?? '');
|
||||
$vars[$i] = nl2br($vars[$i]);
|
||||
}
|
||||
} else {
|
||||
foreach ($vars as $i => $var) {
|
||||
$vars[$i] = nl2br($var ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$result = strtr($text, $vars);
|
||||
|
||||
// Remove non-replaced placeholders.
|
||||
if ($remove_non_replaced) {
|
||||
$result = preg_replace('#\{%[^\.%\}]+\.[^%\}]+%\}#', '', $result ?? '');
|
||||
$result = trim($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,7 +252,7 @@ class Mail
|
||||
*/
|
||||
public static function hasVars($text)
|
||||
{
|
||||
return preg_match('/({%|%})/', $text);
|
||||
return preg_match('/({%|%})/', $text ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,14 +284,29 @@ class Mail
|
||||
return Option::get('mail_driver', 'mail');
|
||||
}
|
||||
|
||||
public static function registerSmtpLogger()
|
||||
{
|
||||
$logger = new \Swift_Plugins_Loggers_ArrayLogger();
|
||||
\Mail::getSwiftMailer()->registerPlugin(new \Swift_Plugins_LoggerPlugin($logger));
|
||||
|
||||
return $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send test email from mailbox.
|
||||
*/
|
||||
public static function sendTestMail($to, $mailbox = null)
|
||||
{
|
||||
$result = [
|
||||
'status' => 'success',
|
||||
'msg' => '',
|
||||
'log' => '',
|
||||
];
|
||||
|
||||
if ($mailbox) {
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\MailHelper::setMailDriver($mailbox);
|
||||
$smtp_logger = self::registerSmtpLogger();
|
||||
|
||||
$status_message = '';
|
||||
|
||||
@@ -254,6 +319,7 @@ class Mail
|
||||
} else {
|
||||
// System email
|
||||
\MailHelper::setSystemMailDriver();
|
||||
$smtp_logger = self::registerSmtpLogger();
|
||||
|
||||
$status_message = '';
|
||||
|
||||
@@ -269,15 +335,17 @@ class Mail
|
||||
if (\Mail::failures() || $status_message) {
|
||||
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_SEND_ERROR, null, null, $status_message);
|
||||
if ($status_message) {
|
||||
throw new \Exception($status_message, 1);
|
||||
} else {
|
||||
return false;
|
||||
$result['msg'] = $status_message;
|
||||
}
|
||||
$result['status'] = 'error';
|
||||
$result['log'] = $smtp_logger->dump();
|
||||
} else {
|
||||
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_ACCEPTED);
|
||||
|
||||
return true;
|
||||
$result['status'] = 'success';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,15 +353,7 @@ class Mail
|
||||
*/
|
||||
public static function fetchTest($mailbox)
|
||||
{
|
||||
$client = new Client([
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
@@ -307,7 +367,11 @@ class Mail
|
||||
// Get unseen messages for a period
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
|
||||
|
||||
$last_error = $client->getLastError();
|
||||
$last_error = '';
|
||||
if (method_exists($client, 'getLastError')) {
|
||||
$last_error = $client->getLastError();
|
||||
}
|
||||
|
||||
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
|
||||
// Solution for MS mailboxes.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/176
|
||||
@@ -338,7 +402,7 @@ class Mail
|
||||
if (is_array($emails)) {
|
||||
$emails_array = $emails;
|
||||
} else {
|
||||
$emails_array = explode(',', $emails);
|
||||
$emails_array = explode(',', $emails ?? '');
|
||||
}
|
||||
|
||||
foreach ($emails_array as $i => $email) {
|
||||
@@ -432,7 +496,7 @@ class Mail
|
||||
*/
|
||||
public static function fetchMessageMarkerValue($body)
|
||||
{
|
||||
preg_match('/{#FS:([^#]+)#}/', $body, $matches);
|
||||
preg_match('/{#FS:([^#]+)#}/', $body ?? '', $matches);
|
||||
if (!empty($matches[1]) && base64_decode($matches[1])) {
|
||||
// Return first found marker.
|
||||
return base64_decode($matches[1]);
|
||||
@@ -441,6 +505,11 @@ class Mail
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function getMessageIdHash($thread_id)
|
||||
{
|
||||
return substr(md5($thread_id.config('app.key')), 0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect autoresponder by headers.
|
||||
* https://github.com/jpmckinney/multi_mail/wiki/Detecting-autoresponders
|
||||
@@ -453,9 +522,11 @@ class Mail
|
||||
$autoresponder_headers = [
|
||||
'x-autoreply' => '',
|
||||
'x-autorespond' => '',
|
||||
'auto-submitted' => 'auto-replied',
|
||||
'auto-submitted' => '', // this can be auto-replied, auto-generated, etc.
|
||||
'precedence' => ['auto_reply', 'bulk', 'junk'],
|
||||
'x-precedence' => ['auto_reply', 'bulk', 'junk'],
|
||||
];
|
||||
$headers = explode("\n", $headers_str);
|
||||
$headers = explode("\n", $headers_str ?? '');
|
||||
|
||||
foreach ($autoresponder_headers as $auto_header => $auto_header_value) {
|
||||
foreach ($headers as $header) {
|
||||
@@ -469,6 +540,12 @@ class Mail
|
||||
if (strtolower($name) == $auto_header) {
|
||||
if (!$auto_header_value) {
|
||||
return true;
|
||||
} elseif (is_array($auto_header_value)) {
|
||||
foreach ($auto_header_value as $auto_header_value_item) {
|
||||
if ($value == $auto_header_value_item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} elseif ($value == $auto_header_value) {
|
||||
return true;
|
||||
}
|
||||
@@ -541,14 +618,487 @@ class Mail
|
||||
*/
|
||||
public static function getMailboxClient($mailbox)
|
||||
{
|
||||
return new \Webklex\IMAP\Client([
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
$oauth = $mailbox->oauthEnabled();
|
||||
$new_library = config('app.new_fetching_library');
|
||||
|
||||
if (!$new_library) {
|
||||
// Old.
|
||||
return new \Webklex\IMAP\Client([
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
} else {
|
||||
// New
|
||||
if ($oauth) {
|
||||
\Config::set('imap.accounts.default', [
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->email,
|
||||
'password' => $mailbox->oauthGetParam('a_token'),
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
'authentication' => 'oauth',
|
||||
]);
|
||||
} else {
|
||||
\Config::set('imap.accounts.default', [
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
// 'username' => $mailbox->email,
|
||||
// 'password' => $mailbox->oauthGetParam('a_token'),
|
||||
// 'protocol' => $mailbox->getInProtocolName(),
|
||||
// 'authentication' => 'oauth',
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
}
|
||||
// To enable debug: /vendor/webklex/php-imap/src/Connection/Protocols
|
||||
// Debug in console
|
||||
if (app()->runningInConsole()) {
|
||||
\Config::set('imap.options.debug', config('app.debug'));
|
||||
}
|
||||
|
||||
$cm = new \Webklex\PHPIMAP\ClientManager(config('imap'));
|
||||
|
||||
// Refresh Access Token.
|
||||
if ($oauth) {
|
||||
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'refresh_token' => $mailbox->oauthGetParam('r_token'),
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
|
||||
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
|
||||
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
|
||||
'error' => $error_message,
|
||||
'mailbox' => $mailbox->name,
|
||||
]);
|
||||
throw new \Exception($error_message, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This makes it authenticate two times.
|
||||
//$cm->setTimeout(60);
|
||||
|
||||
return $cm->account('default');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate artificial Message-ID.
|
||||
*/
|
||||
public static function generateMessageId($email_address, $raw_body = '')
|
||||
{
|
||||
$hash = str_random(16);
|
||||
if ($raw_body) {
|
||||
$hash = md5(strval($raw_body));
|
||||
}
|
||||
|
||||
return 'fs-'.$hash.'@'.preg_replace("/.*@/", '', $email_address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch IMAP message by Message-ID.
|
||||
*/
|
||||
public static function fetchMessage($mailbox, $message_id, $message_date = null)
|
||||
{
|
||||
$no_charset = false;
|
||||
|
||||
if (!$message_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->connect();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
|
||||
return null;
|
||||
}
|
||||
|
||||
$imap_folders = \Eventy::filter('mail.fetch_message.imap_folders', $mailbox->getInImapFolders(), $mailbox);
|
||||
|
||||
foreach ($imap_folders as $folder_name) {
|
||||
try {
|
||||
$folder = self::getImapFolder($client, $folder_name);
|
||||
|
||||
if (!$folder) {
|
||||
\Log::error('('.$mailbox->name.') Show Original - folder not found: '.$folder_name);
|
||||
continue;
|
||||
}
|
||||
// Message-ID: <123@123.com>
|
||||
$query = $folder->query()
|
||||
->text('<'.$message_id.'>')
|
||||
->leaveUnread()
|
||||
->limit(1);
|
||||
|
||||
// Limit using date to speed up the search.
|
||||
if ($message_date) {
|
||||
$query->since($message_date->subDays(7));
|
||||
// Here we should add 14 days, as previous line subtracts 7 days.
|
||||
$query->before($message_date->addDays(14));
|
||||
}
|
||||
|
||||
if ($no_charset) {
|
||||
$query->setCharset(null);
|
||||
}
|
||||
|
||||
$messages = $query->get();
|
||||
|
||||
$last_error = '';
|
||||
if (method_exists($client, 'getLastError')) {
|
||||
$last_error = $client->getLastError();
|
||||
}
|
||||
|
||||
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
|
||||
// Solution for MS mailboxes.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/176
|
||||
$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null);
|
||||
if ($message_date) {
|
||||
$query->since($message_date->subDays(7));
|
||||
$query->before($message_date->addDays(14));
|
||||
}
|
||||
$messages = $query->get();
|
||||
$no_charset = true;
|
||||
}
|
||||
|
||||
if (count($messages)) {
|
||||
return $messages->first();
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function oauthGetAuthorizationUrl($provider_code, $params)
|
||||
{
|
||||
$args = [];
|
||||
|
||||
switch ($provider_code) {
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
// https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
|
||||
$args = [
|
||||
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
|
||||
'response_type' => 'code',
|
||||
'approval_prompt' => 'auto',
|
||||
'redirect_uri' => route('mailboxes.oauth_callback'),
|
||||
];
|
||||
$args = array_merge($args, $params);
|
||||
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?'.http_build_query($args);
|
||||
break;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public static function oauthGetAccessToken($provider_code, $params)
|
||||
{
|
||||
$token_data = [];
|
||||
$post_params = [];
|
||||
|
||||
switch ($provider_code) {
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
$post_params = [
|
||||
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
|
||||
"grant_type" => "authorization_code",
|
||||
'redirect_uri' => route('mailboxes.oauth_callback'),
|
||||
];
|
||||
|
||||
$post_params = array_merge($post_params, $params);
|
||||
|
||||
// Refreshing Access Token.
|
||||
if (!empty($post_params['refresh_token'])) {
|
||||
$post_params['grant_type'] = 'refresh_token';
|
||||
}
|
||||
|
||||
// $postUrl = "/common/oauth2/token";
|
||||
// $hostname = "login.microsoftonline.com";
|
||||
$full_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
||||
|
||||
// $headers = array(
|
||||
// // "POST " . $postUrl . " HTTP/1.1",
|
||||
// // "Host: login.windows.net",
|
||||
// "Content-type: application/x-www-form-urlencoded",
|
||||
// );
|
||||
|
||||
$curl = curl_init($full_url);
|
||||
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
//curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $post_params);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array("application/x-www-form-urlencoded"));
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
\Helper::setCurlDefaultOptions($curl);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 180);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
|
||||
if ($response) {
|
||||
$result = json_decode($response, true);
|
||||
|
||||
// [token_type] => Bearer
|
||||
// [scope] => IMAP.AccessAsUser.All offline_access SMTP.Send User.Read
|
||||
// [expires_in] => 4514
|
||||
// [ext_expires_in] => 4514
|
||||
// [expires_on] => 1646122657
|
||||
// [not_before] => 1646117842
|
||||
// [resource] => 00000002-0000-0000-c000-000000000000
|
||||
// [access_token] => dd
|
||||
// [refresh_token] => dd
|
||||
// [id_token] => dd
|
||||
if (!empty($result['access_token'])) {
|
||||
$token_data['provider'] = self::OAUTH_PROVIDER_MICROSOFT;
|
||||
$token_data['a_token'] = $result['access_token'];
|
||||
$token_data['r_token'] = $result['refresh_token'];
|
||||
//$token_data['id_token'] = $result['id_token'];
|
||||
$token_data['issued_on'] = now()->toDateTimeString();
|
||||
$token_data['expires_in'] = $result['expires_in'];
|
||||
} elseif ($response) {
|
||||
$token_data['error'] = $response;
|
||||
} else {
|
||||
$token_data['error'] = 'Response code: '.curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $token_data;
|
||||
}
|
||||
|
||||
public static function oauthDisconnect($provider_code, $redirect_uri)
|
||||
{
|
||||
switch ($provider_code) {
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
return redirect()->away('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri='.urlencode($redirect_uri));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static function prepareMailable($mailable)
|
||||
{
|
||||
$custom_headers_str = config('app.custom_mail_headers');
|
||||
|
||||
if (empty($custom_headers_str)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$custom_headers = explode(';', $custom_headers_str);
|
||||
|
||||
$mailable->withSwiftMessage(function ($swiftmessage) use ($custom_headers) {
|
||||
$headers = $swiftmessage->getHeaders();
|
||||
|
||||
foreach ($custom_headers as $custom_header) {
|
||||
$header_parts = explode(':', $custom_header);
|
||||
|
||||
$header_name = trim($header_parts[0] ?? '');
|
||||
$header_value = trim($header_parts[1] ?? '');
|
||||
if ($header_name && $header_value) {
|
||||
$headers->addTextHeader($header_name, $header_value);
|
||||
}
|
||||
}
|
||||
return $swiftmessage;
|
||||
});
|
||||
}
|
||||
|
||||
public static function getImapFolder($client, $folder_name)
|
||||
{
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3502
|
||||
$folder_name = mb_convert_encoding($folder_name, "UTF7-IMAP","UTF-8");
|
||||
|
||||
if (method_exists($client, 'getFolderByPath')) {
|
||||
return $client->getFolderByPath($folder_name);
|
||||
} else {
|
||||
return $client->getFolder($folder_name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to decode email subjects and attachment names in Webklex libraries.
|
||||
*/
|
||||
public static function decodeSubject($subject)
|
||||
{
|
||||
// Remove new lines as iconv_mime_decode() may loose a part separated by new line:
|
||||
// =?utf-8?Q?Gesch=C3=A4ftskonto?= erstellen =?utf-8?Q?f=C3=BCr?=
|
||||
// 249143
|
||||
$subject = preg_replace("/[\r\n]/", '', $subject);
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3185
|
||||
$subject = str_ireplace('=?iso-2022-jp?', '=?iso-2022-jp-ms?', $subject);
|
||||
|
||||
// Sometimes imap_utf8() can't decode the subject, for example:
|
||||
// =?iso-2022-jp?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?=
|
||||
// and sometimes iconv_mime_decode() can't decode the subject.
|
||||
// So we are using both.
|
||||
//
|
||||
// We are trying iconv_mime_decode() first because imap_utf8()
|
||||
// decodes umlauts into two symbols:
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2965
|
||||
|
||||
// Sometimes subject is split into parts and each part is base63 encoded.
|
||||
// And sometimes it's first encoded and after that split.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3066
|
||||
|
||||
// Step 1. Abnormal way - text is encoded and split into parts.
|
||||
|
||||
// Only one type of encoding should be used.
|
||||
preg_match_all("/(=\?[^\?]+\?[BQ]\?)([^\?]+)(\?=)/i", $subject, $m);
|
||||
$encodings = $m[1] ?? [];
|
||||
array_walk($encodings, function($value) {
|
||||
$value = strtolower($value);
|
||||
});
|
||||
$one_encoding = count(array_unique($encodings)) == 1;
|
||||
|
||||
if ($one_encoding) {
|
||||
// First try to join all lines and parts.
|
||||
// Keep in mind that there can be non-encoded parts also:
|
||||
// =?utf-8?Q?Gesch=C3=A4ftskonto?= erstellen =?utf-8?Q?f=C3=BCr?=
|
||||
preg_match_all("/(=\?[^\?]+\?[BQ]\?)([^\?]+)(\?=)[\r\n\t ]*/i", $subject, $m);
|
||||
|
||||
$joined_parts = '';
|
||||
if (count($m[1]) > 1 && !empty($m[2]) && !preg_match("/[\r\n\t ]+[^=]/i", $subject)) {
|
||||
// Example: GyRCQGlNVTtZRTkhIT4uTlMbKEI=
|
||||
$joined_parts = $m[1][0].implode('', $m[2]).$m[3][0];
|
||||
|
||||
// Base64 and URL encoded string can't contain "=" in the middle
|
||||
// https://stackoverflow.com/questions/6916805/why-does-a-base64-encoded-string-have-an-sign-at-the-end
|
||||
$has_equal_in_the_middle = preg_match("#=+([^$\? =])#", $joined_parts);
|
||||
|
||||
if (!$has_equal_in_the_middle) {
|
||||
$subject_decoded = iconv_mime_decode($joined_parts, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
|
||||
|
||||
if ($subject_decoded
|
||||
&& trim($subject_decoded) != trim($joined_parts)
|
||||
&& trim($subject_decoded) != trim(rtrim($joined_parts, '='))
|
||||
&& !self::isNotYetFullyDecoded($subject_decoded)
|
||||
) {
|
||||
return $subject_decoded;
|
||||
}
|
||||
|
||||
// Try imap_utf8().
|
||||
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQ?= =?iso-2022-jp?B?C4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
|
||||
$subject_decoded = \imap_utf8($joined_parts);
|
||||
|
||||
if ($subject_decoded
|
||||
&& trim($subject_decoded) != trim($joined_parts)
|
||||
&& trim($subject_decoded) != trim(rtrim($joined_parts, '='))
|
||||
&& !self::isNotYetFullyDecoded($subject_decoded)
|
||||
) {
|
||||
return $subject_decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2. Standard way - each part is encoded separately.
|
||||
|
||||
// iconv_mime_decode() can't decode:
|
||||
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
|
||||
$subject_decoded = iconv_mime_decode($subject, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
|
||||
|
||||
// Sometimes iconv_mime_decode() can't decode some parts of the subject:
|
||||
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
|
||||
// =?iso-2022-jp?B?GyRCQGlNVTtZRTkhIT4uTlMbKEI=?=
|
||||
if (self::isNotYetFullyDecoded($subject_decoded)) {
|
||||
$subject_decoded = \imap_utf8($subject);
|
||||
}
|
||||
|
||||
// All previous functions could not decode text.
|
||||
// mb_decode_mimeheader() properly decodes umlauts into one unice symbol.
|
||||
// But we use mb_decode_mimeheader() as a last resort as it may garble some symbols.
|
||||
// Example: =?ISO-8859-1?Q?Vorgang 538336029: M=F6chten Sie Ihre E-Mail-Adresse =E4ndern??=
|
||||
if (self::isNotYetFullyDecoded($subject_decoded)) {
|
||||
$subject_decoded = mb_decode_mimeheader($subject);
|
||||
}
|
||||
|
||||
if (!$subject_decoded) {
|
||||
$subject_decoded = $subject;
|
||||
}
|
||||
|
||||
return $subject_decoded;
|
||||
}
|
||||
|
||||
public static function isNotYetFullyDecoded($subject_decoded) {
|
||||
// https://stackoverflow.com/questions/15276191/why-does-a-diamond-with-a-questionmark-in-it-appear-in-my-html
|
||||
$invalid_utf_symbols = ['<27>'];
|
||||
|
||||
return preg_match_all("/=\?[^\?]+\?[BQ]\?/i", $subject_decoded)
|
||||
|| !mb_check_encoding($subject_decoded, 'UTF-8')
|
||||
|| \Str::contains($subject_decoded, $invalid_utf_symbols);
|
||||
}
|
||||
|
||||
public static function getHashedReplySeparator($message_id) {
|
||||
$separator = \MailHelper::REPLY_SEPARATOR_HTML;
|
||||
|
||||
if ($message_id) {
|
||||
$separator .= substr(md5($message_id.config('app.key')), 0, 8);
|
||||
}
|
||||
|
||||
return $separator;
|
||||
}
|
||||
|
||||
// Sanitize status message - remove SMTP username and password.
|
||||
public static function sanitizeSmtpStatusMessage($status_message) {
|
||||
$status_message = preg_replace('#(username ")[^"]+(")#', '$1***$2', $status_message ?? '');
|
||||
$status_message = preg_replace("#(Swift_Transport_Esmtp_Auth_LoginAuthenticator\->authenticate\(Object\(Swift_SmtpTransport\), ')[^\']+(', ')[^\']+('\))#", '$1***$2***$3', $status_message ?? '');
|
||||
|
||||
return $status_message;
|
||||
}
|
||||
|
||||
public static function parseEml($content, $mailbox) {
|
||||
if (!str_contains($content, "\r\n")){
|
||||
$content = str_replace("\n", "\r\n", $content);
|
||||
}
|
||||
|
||||
$raw_header = substr($content, 0, strpos($content, "\r\n\r\n"));
|
||||
$raw_body = substr($content, strlen($raw_header)+8);
|
||||
|
||||
\Config::set('app.new_fetching_library', 'true');
|
||||
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->openFolder("INBOX");
|
||||
|
||||
return \Webklex\PHPIMAP\Message::make(null, null, $client, $raw_header, $raw_body, [], \Webklex\PHPIMAP\IMAP::ST_UID);
|
||||
}
|
||||
|
||||
// public static function oauthGetProvider($provider_code, $params)
|
||||
// {
|
||||
// $provider = null;
|
||||
|
||||
// switch ($provider_code) {
|
||||
// case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
// $provider = new \Stevenmaguire\OAuth2\Client\Provider\Microsoft([
|
||||
// // Required
|
||||
// 'clientId' => $params['client_id'],
|
||||
// 'clientSecret' => $params['client_secret'],
|
||||
// 'redirectUri' => route('mailboxes.oauth_callback'),
|
||||
// //https://login.microsoftonline.com/common/oauth2/authorize';
|
||||
// 'urlAuthorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
// 'urlAccessToken' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
// 'urlResourceOwnerDetails' => 'https://outlook.office.com/api/v1.0/me'
|
||||
// ]);
|
||||
// break;
|
||||
// }
|
||||
|
||||
// return $provider;
|
||||
// }
|
||||
}
|
||||
|
||||
19
app/Misc/SwiftGetSmtpQueueId.php
Normal file
19
app/Misc/SwiftGetSmtpQueueId.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Misc;
|
||||
|
||||
class SwiftGetSmtpQueueId implements \Swift_Events_ResponseListener
|
||||
{
|
||||
public static $last_smtp_queue_id = null;
|
||||
|
||||
public function responseReceived(\Swift_Events_ResponseEvent $evt)
|
||||
{
|
||||
$response_text = $evt->getResponse();
|
||||
if (strpos($response_text, 'queued') !== false) {
|
||||
preg_match("#queued as ([^\$\r\n ]+)[$\r\n]#", $response_text, $m);
|
||||
if (!empty($m[1]) && trim($m[1])) {
|
||||
self::$last_smtp_queue_id = trim($m[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,36 +2,68 @@
|
||||
|
||||
namespace App\Misc;
|
||||
|
||||
use Zttp\Zttp;
|
||||
|
||||
class WpApi
|
||||
{
|
||||
const ENDPOINT_MODULES = 'freescout/v1/modules';
|
||||
|
||||
const METHOD_GET = 'get';
|
||||
const METHOD_POST = 'post';
|
||||
const METHOD_GET = 'GET';
|
||||
const METHOD_POST = 'POST';
|
||||
|
||||
const ACTION_CHECK_LICENSE = 'check_license';
|
||||
const ACTION_CHECK_LICENSES = 'check_licenses';
|
||||
const ACTION_ACTIVATE_LICENSE = 'activate_license';
|
||||
const ACTION_DEACTIVATE_LICENSE = 'deactivate_license';
|
||||
const ACTION_GET_VERSION = 'get_version';
|
||||
|
||||
public static $lastError;
|
||||
|
||||
public static function url($path)
|
||||
public static function url($path, $alternative = false)
|
||||
{
|
||||
return \Config::get('app.freescout_api').$path;
|
||||
if ($alternative) {
|
||||
return \Config::get('app.freescout_alt_api').$path;
|
||||
} else {
|
||||
return \Config::get('app.freescout_api').$path;
|
||||
}
|
||||
}
|
||||
|
||||
public static function httpRequest($method, $url, $params)
|
||||
{
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
if ($method == self::METHOD_POST) {
|
||||
if (strstr($url, '?')) {
|
||||
$url .= '&';
|
||||
} else {
|
||||
$url .= '?';
|
||||
}
|
||||
$url .= 'v='.config('app.version');
|
||||
return $client->request('POST', $url, \Helper::setGuzzleDefaultOptions([
|
||||
'connect_timeout' => 10,
|
||||
'form_params' => $params,
|
||||
]));
|
||||
} else {
|
||||
$params['v'] = config('app.version');
|
||||
return $client->request('GET', $url, \Helper::setGuzzleDefaultOptions([
|
||||
'connect_timeout' => 10,
|
||||
'query' => $params,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API request.
|
||||
*/
|
||||
public static function request($method, $endpoint, $params = [])
|
||||
public static function request($method, $endpoint, $params = [], $alternative_api = false)
|
||||
{
|
||||
self::$lastError = null;
|
||||
|
||||
try {
|
||||
$response = Zttp::$method(self::url($endpoint), $params);
|
||||
$response = self::httpRequest($method, self::url($endpoint, $alternative_api), $params);
|
||||
} catch (\Exception $e) {
|
||||
if (!$alternative_api) {
|
||||
return self::request($method, $endpoint, $params, true);
|
||||
}
|
||||
\Helper::logException($e, 'WpApi');
|
||||
self::$lastError = [
|
||||
'code' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
@@ -41,8 +73,9 @@ class WpApi
|
||||
}
|
||||
|
||||
// https://guzzle3.readthedocs.io/http-client/response.html
|
||||
if ($response->status() < 500) {
|
||||
$json = $response->json();
|
||||
if ($response->getStatusCode() < 500) {
|
||||
$json = \Helper::jsonToArray($response->getBody());
|
||||
|
||||
if (!empty($json['code']) && !empty($json['message']) &&
|
||||
!empty($json['data']) && !empty($json['data']['status']) && $json['data']['status'] != 200
|
||||
) {
|
||||
@@ -81,6 +114,18 @@ class WpApi
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check module license.
|
||||
*/
|
||||
public static function checkLicenses($params)
|
||||
{
|
||||
$params['action'] = self::ACTION_CHECK_LICENSES;
|
||||
|
||||
$endpoint = self::ENDPOINT_MODULES;
|
||||
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate module license.
|
||||
*/
|
||||
@@ -97,6 +142,22 @@ class WpApi
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate module license.
|
||||
*/
|
||||
public static function deactivateLicense($params)
|
||||
{
|
||||
$params['action'] = self::ACTION_DEACTIVATE_LICENSE;
|
||||
|
||||
$endpoint = self::ENDPOINT_MODULES;
|
||||
|
||||
if (!empty($params['module_alias'])) {
|
||||
$endpoint .= '/'.$params['module_alias'];
|
||||
}
|
||||
|
||||
return self::request(self::METHOD_POST, $endpoint, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license details.
|
||||
*/
|
||||
|
||||
386
app/Module.php
386
app/Module.php
@@ -6,7 +6,9 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Misc\WpApi;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
class Module extends Model
|
||||
{
|
||||
@@ -74,7 +76,7 @@ class Module extends Model
|
||||
|
||||
public static function isOfficial($author_url)
|
||||
{
|
||||
return parse_url($author_url, PHP_URL_HOST) == parse_url(\Config::get('app.freescout_url'), PHP_URL_HOST);
|
||||
return parse_url($author_url ?? '', PHP_URL_HOST) == parse_url(\Config::get('app.freescout_url') ?? '', PHP_URL_HOST);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +183,7 @@ class Module extends Model
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
$list = explode(',', $required_extensions);
|
||||
$list = explode(',', $required_extensions ?? '');
|
||||
if (!is_array($list) || !count($list)) {
|
||||
return [];
|
||||
}
|
||||
@@ -194,4 +196,384 @@ class Module extends Model
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check missing modules required by the module.
|
||||
*/
|
||||
public static function getMissingModules($required_modules, $modules = [])
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
if (!$modules) {
|
||||
$modules = \Module::all();
|
||||
}
|
||||
|
||||
if (!is_array($required_modules) || !count($required_modules)) {
|
||||
return [];
|
||||
}
|
||||
foreach ($required_modules as $alias => $version) {
|
||||
$module = null;
|
||||
foreach ($modules as $module_item) {
|
||||
if ($module_item->alias == $alias) {
|
||||
$module = $module_item;
|
||||
}
|
||||
}
|
||||
if (!$module) {
|
||||
$missing[$alias] = $version;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!self::isActive($alias) || !version_compare($module->version, $version, '>=')) {
|
||||
$missing[$alias] = $version;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public static function formatName($name)
|
||||
{
|
||||
return preg_replace("/ Module($|.*\[.*\]$)/", '$1', $name);
|
||||
}
|
||||
|
||||
public static function formatModuleData($module_data)
|
||||
{
|
||||
// Add (Third-Party).
|
||||
if (\App\Module::isOfficial($module_data['authorUrl'])
|
||||
&& $module_data['author'] != 'FreeScout'
|
||||
&& mb_substr(trim($module_data['name']), -1) != ']'
|
||||
) {
|
||||
$module_data['name'] = $module_data['name'].' ['.__('Third-Party').']';
|
||||
}
|
||||
return $module_data;
|
||||
}
|
||||
|
||||
public static function isThirdParty($module_data)
|
||||
{
|
||||
if (\App\Module::isOfficial($module_data['authorUrl'])
|
||||
&& $module_data['author'] != 'FreeScout'
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSymlinkPath($alias)
|
||||
{
|
||||
return public_path().\Module::getPublicPath($alias);
|
||||
}
|
||||
|
||||
// Check and try to fix invalid or missing symlinks.
|
||||
public static function checkSymlinks($module_aliases = null)
|
||||
{
|
||||
$invalid_symlinks = [];
|
||||
|
||||
if ($module_aliases === null) {
|
||||
// Get all active modules.
|
||||
foreach (\Module::all() as $module) {
|
||||
if ($module->active()) {
|
||||
$module_aliases[] = $module->getAlias();
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($module_aliases && count($module_aliases)) {
|
||||
foreach ($module_aliases as $module_alias) {
|
||||
$from = self::getSymlinkPath($module_alias);
|
||||
|
||||
$create = false;
|
||||
|
||||
// file_exists() also checks if symlink target exists.
|
||||
// file_exists() and is_dir() may throw "open_basedir restriction in effect".
|
||||
try {
|
||||
if (!file_exists($from) || !is_link($from)) {
|
||||
if (is_dir($from)) {
|
||||
@rename($from, $from.'_'.date('YmdHis'));
|
||||
} else {
|
||||
@unlink($from);
|
||||
}
|
||||
$create = true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$create = true;
|
||||
}
|
||||
|
||||
// Skip this check.
|
||||
// elseif (is_link($from) && readlink($symlink_path) != '') {
|
||||
// // Symlink leads to the wrong place.
|
||||
// $create = true;
|
||||
// }
|
||||
|
||||
// Try to create the symlink.
|
||||
if ($create) {
|
||||
$to = self::createModuleSymlink($module_alias);
|
||||
|
||||
if ($to && (!is_link($from) || is_link($to) || !file_exists($from))) {
|
||||
$invalid_symlinks[$from] = $to;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $invalid_symlinks;
|
||||
}
|
||||
|
||||
// There is similar function in ModuleInstall.php
|
||||
public static function createModuleSymlink($alias)
|
||||
{
|
||||
$from = self::getSymlinkPath($alias);
|
||||
|
||||
$module = \Module::findByAlias($alias);
|
||||
if (!$module) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$to = $module->getExtraPath('Public');
|
||||
|
||||
// file_exists() may throw "open_basedir restriction in effect".
|
||||
try {
|
||||
// If module's Public is symlink.
|
||||
if (is_link($to)) {
|
||||
@unlink($to);
|
||||
}
|
||||
|
||||
// Symlimk may exist but lead to the module folder in a wrong case.
|
||||
// So we need first try to remove it.
|
||||
if (!file_exists($from)) {
|
||||
@unlink($from);
|
||||
}
|
||||
|
||||
if (file_exists($from)) {
|
||||
return $to;
|
||||
}
|
||||
|
||||
if (!file_exists($to)) {
|
||||
// Try to create Public folder.
|
||||
try {
|
||||
\File::makeDirectory($to, \Helper::DIR_PERMISSIONS);
|
||||
} catch (\Exception $e) {
|
||||
// If it's a broken symlink.
|
||||
if (is_link($to)) {
|
||||
@unlink($to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
symlink($to, $from);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
|
||||
//return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $to;
|
||||
}
|
||||
|
||||
public static function updateModule($alias)
|
||||
{
|
||||
$result = [
|
||||
'status' => 'error',
|
||||
// Error message.
|
||||
'msg' => '',
|
||||
'msg_success' => '',
|
||||
'download_error' => false,
|
||||
// Error message with the link for downloading the module.
|
||||
'download_msg' => '',
|
||||
'output' => '',
|
||||
'module_name' => '',
|
||||
];
|
||||
|
||||
$module = \Module::findByAlias($alias);
|
||||
|
||||
if (!$module) {
|
||||
$result['msg'] = __('Module not found').': '.$alias;
|
||||
}
|
||||
|
||||
// Get module name.
|
||||
$name = '?';
|
||||
if ($module) {
|
||||
$name = $module->getName();
|
||||
$result['module_name'] = $name;
|
||||
}
|
||||
|
||||
// Download new version.
|
||||
if (!$result['msg']) {
|
||||
// Check if the module's author is officially recognized
|
||||
if (self::isOfficial($module->authorUrl)) {
|
||||
$params = [
|
||||
'license' => self::getLicense($alias),
|
||||
'module_alias' => $alias,
|
||||
'url' => self::getAppUrl(),
|
||||
];
|
||||
$license_details = WpApi::getVersion($params);
|
||||
|
||||
if (WpApi::$lastError) {
|
||||
$result['msg'] = WpApi::$lastError['message'];
|
||||
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||
$result['msg'] = $license_details['message'];
|
||||
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
|
||||
$result['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
|
||||
} elseif (!empty($license_details['download_link'])) {
|
||||
// If a download link is available, proceed to update the module from the URL
|
||||
$result = self::updateFromUrl($module, $license_details['download_link'], $result);
|
||||
} elseif ($license_details['status'] && $result['msg'] = self::getErrorMessage($license_details['status'])) {
|
||||
//$result['msg'] = ;
|
||||
} else {
|
||||
$result['msg'] = __('Error occurred').': '.json_encode($license_details);
|
||||
}
|
||||
} else {
|
||||
// If the module's author is not officially recognized, check for a direct download link
|
||||
$latest_version_zip_url = $module->latestVersionZipUrl ?? null;
|
||||
|
||||
if (!empty($latest_version_zip_url)) {
|
||||
// Update the module from the provided ZIP URL
|
||||
$result = self::updateFromUrl($module, $module->latestVersionZipUrl, $result);
|
||||
} else {
|
||||
// If no download link is available, set an error message indicating the module cannot be downloaded
|
||||
$result['msg'] = __('Error occurred') . ': module not available for download';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run post-update instructions.
|
||||
if (!$result['msg'] && !$result['download_error']) {
|
||||
$output_log = new BufferedOutput();
|
||||
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $output_log);
|
||||
$result['output'] = $output_log->fetch() ?: ' ';
|
||||
|
||||
$result['msg'] = __('Error occurred activating ":name" module', ['name' => $name]);
|
||||
|
||||
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||
// Error.
|
||||
// If there was any error, module has been deactivated via modules.register_error filter
|
||||
$result['msg'] = '';
|
||||
foreach (session('flashes_floating') as $flash) {
|
||||
$result['msg'] .= $flash['text'].' ';
|
||||
}
|
||||
} elseif (strstr($result['output'], 'Configuration cached successfully')) {
|
||||
// Success.
|
||||
$result['status'] = 'success';
|
||||
$result['msg'] = '';
|
||||
$result['msg_success'] = __('":name" module successfully updated!', ['name' => $name]);
|
||||
} else {
|
||||
// Error.
|
||||
// Deactivate module.
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a module from a given URL.
|
||||
*
|
||||
* @param Module $module The module to be updated.
|
||||
* @param string $url The URL where the new version of the module can be downloaded.
|
||||
* @param array $result An associative array to store the result of the update operation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function updateFromUrl($module, $url, $result)
|
||||
{
|
||||
$alias = $module->alias;
|
||||
// Download module.
|
||||
$module_archive = \Module::getPath() . DIRECTORY_SEPARATOR . $alias . '.zip';
|
||||
|
||||
try {
|
||||
\Helper::downloadRemoteFile($url, $module_archive);
|
||||
} catch (\Exception $e) {
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if (!file_exists($module_archive)) {
|
||||
$result['download_error'] = true;
|
||||
} else {
|
||||
// Extract.
|
||||
try {
|
||||
// Sometimes by some reason Public folder becomes a symlink leading to itself.
|
||||
// It causes an error during updating process.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2709
|
||||
$public_folder = $module->getPath() . DIRECTORY_SEPARATOR . 'Public';
|
||||
try {
|
||||
if (is_link($public_folder)) {
|
||||
unlink($public_folder);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
\Helper::unzip($module_archive, \Module::getPath());
|
||||
} catch (\Exception $e) {
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
// Check if extracted module exists.
|
||||
\Module::clearCache();
|
||||
$module = \Module::findByAlias($alias);
|
||||
if (!$module) {
|
||||
$result['download_error'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove archive.
|
||||
if (file_exists($module_archive)) {
|
||||
\File::delete($module_archive);
|
||||
}
|
||||
|
||||
if ($result['download_error']) {
|
||||
$result['download_msg'] = __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', [
|
||||
'%a_being%' => '<a href="' . $url . '" target="_blank">',
|
||||
'%a_end%' => '</a>',
|
||||
'folder' => '<strong>' . \Module::getPath() . '</strong>',
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getErrorMessage($code, $result = null)
|
||||
{
|
||||
$msg = '';
|
||||
|
||||
switch ($code) {
|
||||
case 'missing':
|
||||
$msg = __('License key does not exist');
|
||||
break;
|
||||
case 'license_not_activable':
|
||||
$msg = __("You have to activate each bundle's module separately");
|
||||
break;
|
||||
case 'disabled':
|
||||
$msg = __('License key has been revoked');
|
||||
break;
|
||||
case 'no_activations_left':
|
||||
$msg = __('No activations left for this license key').' ('.__("Use 'Deactivate License' link above to transfer license key from another domain").')';
|
||||
break;
|
||||
case 'expired':
|
||||
$msg = __('License key has expired');
|
||||
break;
|
||||
case 'key_mismatch':
|
||||
$msg = __('License key belongs to another module');
|
||||
break;
|
||||
// This also happens when entering a valid license key for wrong module.
|
||||
case 'invalid_item_id':
|
||||
$msg = __('Invalid license key');
|
||||
//$msg = __('Module not found in the modules directory');
|
||||
break;
|
||||
case 'site_inactive':
|
||||
$msg = __('License key is activated on another domain.').' '.__("Use 'Deactivate License' link above to transfer license key from another domain");
|
||||
//$msg = __('Module not found in the modules directory');
|
||||
break;
|
||||
default:
|
||||
if ($result && !empty($result['error'])) {
|
||||
$msg = __('Error code:'.' '.$result['error']);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ class BroadcastNotification extends Notification implements ShouldQueue
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Try to convert to array.
|
||||
$mediums = (array)$payload->mediums;
|
||||
|
||||
$thread = Thread::find($payload->thread_id);
|
||||
|
||||
if (empty($thread)) {
|
||||
@@ -82,7 +85,7 @@ class BroadcastNotification extends Notification implements ShouldQueue
|
||||
$db_notification = new \Illuminate\Notifications\DatabaseNotification();
|
||||
|
||||
// HTML for the menu notification (uses same medium as for email)
|
||||
if (in_array(Subscription::MEDIUM_EMAIL, $payload->mediums)) {
|
||||
if (in_array(Subscription::MEDIUM_EMAIL, $mediums)) {
|
||||
$web_notifications_info = [];
|
||||
|
||||
// Get last reply or note of the conversation to display it's text
|
||||
@@ -114,7 +117,7 @@ class BroadcastNotification extends Notification implements ShouldQueue
|
||||
}
|
||||
|
||||
// Text and url for the browser push notification
|
||||
if (in_array(Subscription::MEDIUM_BROWSER, $payload->mediums)) {
|
||||
if (in_array(Subscription::MEDIUM_BROWSER, $mediums)) {
|
||||
$data['browser']['text'] = strip_tags($thread->getActionDescription($thread->conversation->number));
|
||||
$data['browser']['url'] = $thread->conversation->url(null, $thread->id, ['mark_as_read' => $db_notification->id]);
|
||||
}
|
||||
|
||||
@@ -86,18 +86,18 @@ class WebsiteNotification extends Notification implements ShouldQueue
|
||||
}
|
||||
|
||||
// Get last reply or note of the conversation to display it's text
|
||||
if ($threads) {
|
||||
$last_threads = Thread::whereIn('conversation_id', $threads->pluck('conversation_id')->unique()->toArray())
|
||||
// Select must contain all fields from orderBy() to avoid:
|
||||
// General error: 3065 Expression #1 of ORDER BY clause is not in SELECT
|
||||
->select(['id', 'conversation_id', 'body', 'created_at'])
|
||||
->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
->distinct('conversation_id')
|
||||
->orderBy('created_at')
|
||||
// We can not use groupBy because of "isn't in GROUP BY"
|
||||
//->groupBy('conversation_id')
|
||||
->get();
|
||||
}
|
||||
// if ($threads) {
|
||||
// $last_threads = Thread::whereIn('conversation_id', $threads->pluck('conversation_id')->unique()->toArray())
|
||||
// // Select must contain all fields from orderBy() to avoid:
|
||||
// // General error: 3065 Expression #1 of ORDER BY clause is not in SELECT
|
||||
// ->select(['id', 'conversation_id', 'body', 'created_at'])
|
||||
// ->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
// ->distinct('conversation_id')
|
||||
// ->orderBy('created_at', 'desc')
|
||||
// // We can not use groupBy because of "isn't in GROUP BY"
|
||||
// //->groupBy('conversation_id')
|
||||
// ->get();
|
||||
// }
|
||||
|
||||
// Populate all collected data into array
|
||||
foreach ($notifications as $notification) {
|
||||
@@ -129,14 +129,14 @@ class WebsiteNotification extends Notification implements ShouldQueue
|
||||
continue;
|
||||
}
|
||||
|
||||
$last_thread_body = '';
|
||||
$conversation = null;
|
||||
// $last_thread_body = '';
|
||||
// $conversation = null;
|
||||
|
||||
$last_thread = $last_threads->firstWhere('conversation_id', $thread->conversation_id);
|
||||
if ($last_thread) {
|
||||
$last_thread_body = $last_thread->body;
|
||||
$conversation = $last_thread->conversation;
|
||||
}
|
||||
// $last_thread = $last_threads->firstWhere('conversation_id', $thread->conversation_id);
|
||||
// if ($last_thread) {
|
||||
$last_thread_body = $thread->body;
|
||||
$conversation = $thread->conversation;
|
||||
//}
|
||||
if (empty($conversation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
18
app/Observers/AttachmentObserver.php
Normal file
18
app/Observers/AttachmentObserver.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Attachment;
|
||||
|
||||
class AttachmentObserver
|
||||
{
|
||||
public function created(Attachment $attachment)
|
||||
{
|
||||
\Eventy::action('attachment.created', $attachment);
|
||||
}
|
||||
|
||||
public function deleted(Attachment $attachment)
|
||||
{
|
||||
\Eventy::action('attachment.deleted', $attachment);
|
||||
}
|
||||
}
|
||||
@@ -37,5 +37,13 @@ class ConversationObserver
|
||||
public function deleting(Conversation $conversation)
|
||||
{
|
||||
$conversation->threads()->delete();
|
||||
$conversation->followers()->delete();
|
||||
|
||||
\Eventy::action('conversation.deleting', $conversation);
|
||||
}
|
||||
|
||||
public function updated(Conversation $conversation)
|
||||
{
|
||||
\Eventy::action('conversation.updated', $conversation);
|
||||
}
|
||||
}
|
||||
|
||||
33
app/Observers/CustomerObserver.php
Normal file
33
app/Observers/CustomerObserver.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Customer;
|
||||
use App\CustomerChannel;
|
||||
|
||||
class CustomerObserver
|
||||
{
|
||||
public function updating(Customer $customer)
|
||||
{
|
||||
$phones = $customer->getPhones();
|
||||
// Set numeric phones.
|
||||
$customer->setPhones($phones);
|
||||
}
|
||||
|
||||
public function deleting(Customer $customer)
|
||||
{
|
||||
\Eventy::action('customer.deleting', $customer);
|
||||
}
|
||||
|
||||
public function created(Customer $customer)
|
||||
{
|
||||
if ($customer->channel && $customer->channel_id) {
|
||||
CustomerChannel::create($customer->id, $customer->channel, $customer->channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleted(Customer $customer)
|
||||
{
|
||||
CustomerChannel::where('customer_id', $customer->id)->delete();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user