Compare commits
1911 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc9921108 | ||
|
|
f326b14776 | ||
|
|
afaf1f53ab | ||
|
|
23bb50eb86 | ||
|
|
39824c876d | ||
|
|
ca1a7d0900 | ||
|
|
351eff9b80 | ||
|
|
e037011c55 | ||
|
|
c0ce07a2b9 | ||
|
|
94f11a9155 | ||
|
|
dff234ffbe | ||
|
|
223b4d12dc | ||
|
|
b33fb5e1d3 | ||
|
|
3511e7acb2 | ||
|
|
be4a1234c3 | ||
|
|
823014d4e9 | ||
|
|
310d3290fe | ||
|
|
07765cf8ff | ||
|
|
c4a6585cc7 | ||
|
|
d99d991650 | ||
|
|
6e8682e693 | ||
|
|
87eab76574 | ||
|
|
a011948149 | ||
|
|
b9391d87dc | ||
|
|
7e7f0a0d10 | ||
|
|
8e62d70986 | ||
|
|
02e5fa8535 | ||
|
|
2dc8d061e1 | ||
|
|
efca2acbe2 | ||
|
|
029e5c8a1f | ||
|
|
7383b95864 | ||
|
|
5be99f03f3 | ||
|
|
da522d8d58 | ||
|
|
60ac03df04 | ||
|
|
5a22011fe6 | ||
|
|
09f4908def | ||
|
|
8172db9cdb | ||
|
|
05e7e1e739 | ||
|
|
b2c72775dd | ||
|
|
9eb7457097 | ||
|
|
0b7d1dc06f | ||
|
|
2f9d1bf92e | ||
|
|
de405fe60f | ||
|
|
9bb3ee9bc1 | ||
|
|
5ab6a42808 | ||
|
|
9b5286efca | ||
|
|
78e1067c07 | ||
|
|
c19c4ee217 | ||
|
|
5ef26eaf32 | ||
|
|
16843c9d6a | ||
|
|
a21f5c9d53 | ||
|
|
97cea6cdef | ||
|
|
12c6552259 | ||
|
|
04aaeb4cc1 | ||
|
|
7c3e57aa57 | ||
|
|
f90ba3d6d6 | ||
|
|
4fe2a9333e | ||
|
|
bd59fa5ef5 | ||
|
|
989c729834 | ||
|
|
a57a424067 | ||
|
|
755e4f0cd7 | ||
|
|
a2a0539316 | ||
|
|
e8975d04d6 | ||
|
|
d6eac44275 | ||
|
|
ea39e24727 | ||
|
|
5a290b3ad3 | ||
|
|
ce523c51e7 | ||
|
|
5bcde678d1 | ||
|
|
98e74e53f2 | ||
|
|
bde2da820c | ||
|
|
4b2dd40d99 | ||
|
|
4d7f8d1f50 | ||
|
|
8510ee8224 | ||
|
|
7e8e514e7b | ||
|
|
fb3b0d3a49 | ||
|
|
aaacc6d3fa | ||
|
|
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 | ||
|
|
23cdcffb12 | ||
|
|
944363765a |
21
.env.example
21
.env.example
@@ -9,13 +9,28 @@
|
||||
# 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
|
||||
@@ -29,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
|
||||
5
.gitignore
vendored
5
.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
|
||||
@@ -32,4 +32,5 @@ Thumbs.db
|
||||
/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.
|
||||
|
||||
59
README.md
59
README.md
@@ -2,35 +2,45 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||
<img src="https://raw.githubusercontent.com/freescout-help-desk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||
<br/><br/>
|
||||
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://hits.seeyoufarm.com)
|
||||
[](https://github.com/freescout-help-desk/freescout#requirements) [](https://hits.seeyoufarm.com)
|
||||
|
||||
</div>
|
||||
|
||||
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP7 (Laravel 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 has been developed from scratch and is not using any copyrighted Help Scout or Zendesk materials.
|
||||
**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.
|
||||
|
||||
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)
|
||||
* [Features](#features)
|
||||
* [Mobile Apps](#mobile-apps)
|
||||
* [Requirements](#requirements)
|
||||
* [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, etc.
|
||||
* No limitations on the number of users, tickets, mailboxes, etc.
|
||||
* 100% Mobile-friendly.
|
||||
* Multilingual (English, French, Japanese, Italian, Polish, Portuguese, Russian, Dutch, German, Spanish, Swedish).
|
||||
* Multilingual: English, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, German, Italian, Japanese, Kazakh, 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.
|
||||
@@ -38,22 +48,21 @@
|
||||
* 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://freescout.net/request-feature/).
|
||||
|
||||
## Demo
|
||||
|
||||
**[Live Demo](https://demo.freescout.net)**
|
||||
|
||||
## Mobile Apps
|
||||
|
||||
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.
|
||||
@@ -68,9 +77,11 @@ FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on
|
||||
* PHP 7.1 - 8.x
|
||||
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
|
||||
|
||||
There are no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
|
||||
|
||||
## Installation
|
||||
|
||||
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
|
||||
[Installation Guide](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide)
|
||||
|
||||
Images & one-click installs:
|
||||
|
||||
@@ -78,28 +89,36 @@ Images & one-click installs:
|
||||
* [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)
|
||||
* [Ubuntu](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
|
||||
|
||||
## Cloud Hosted
|
||||
|
||||
[Cloud Hosted FreeScout](https://freescout.net/cloud-hosted/)
|
||||
|
||||
## Modules
|
||||
|
||||
[Available modules](https://freescout.net/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 Integration](https://freescout.net/zapier/)
|
||||
* [Zapier](https://freescout.net/zapier/)
|
||||
* [Make](https://freescout.net/make-integration/) (Integromat)
|
||||
|
||||
## News & Updates
|
||||
|
||||
If you would like to be notified about important news and updates you can subscribe [here](https://freescout.net/subscribe/)
|
||||
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 the project by leaving a feedback](https://github.com/freescout-help-desk/freescout/issues/288)
|
||||
* [Development Guide](https://github.com/freescout-help-desk/freescout/wiki/Development-Guide)
|
||||
* [Todo list](https://github.com/freescout-help-desk/freescout/labels/help%20wanted)
|
||||
* [Translate](https://github.com/freescout-help-desk/freescout/wiki/Translate)
|
||||
|
||||
## 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ class Attachment extends Model
|
||||
|
||||
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,
|
||||
@@ -57,14 +59,48 @@ class Attachment extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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);
|
||||
$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 = preg_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);
|
||||
@@ -114,12 +150,18 @@ class Attachment extends Model
|
||||
|
||||
$file_dir .= $i.DIRECTORY_SEPARATOR;
|
||||
|
||||
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);
|
||||
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()]');
|
||||
}
|
||||
|
||||
\Helper::sanitizeUploadedFileData($file_path, \Helper::getPrivateStorage(), $content);
|
||||
|
||||
return [
|
||||
'file_dir' => $file_dir,
|
||||
'file_path' => $file_path,
|
||||
@@ -192,7 +234,7 @@ class Attachment extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Conver type name to integer.
|
||||
* Convert type name to integer.
|
||||
*/
|
||||
public static function typeNameToInt($type_name)
|
||||
{
|
||||
@@ -210,7 +252,18 @@ class Attachment extends Model
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return Storage::url($this->getStorageFilePath()).'?id='.$this->id.'&token='.$this->getToken();
|
||||
$file_path = $this->getStorageFilePath();
|
||||
|
||||
// URL must contain only forward slashes.
|
||||
$file_path = str_replace(DIRECTORY_SEPARATOR, '/', $file_path);
|
||||
|
||||
$file_url = Storage::url($file_path);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,7 +286,13 @@ class Attachment extends Model
|
||||
if ($view) {
|
||||
$headers['Content-Disposition'] = '';
|
||||
}
|
||||
return $this->getDisk()->download($this->getStorageFilePath(), $this->file_name, $headers);
|
||||
$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() {
|
||||
@@ -270,6 +329,14 @@ class Attachment extends Model
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$size = (int) $size;
|
||||
@@ -325,12 +392,42 @@ class Attachment extends Model
|
||||
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)
|
||||
public function duplicate($thread_id = null)
|
||||
{
|
||||
$new_attachment = $this->replicate();
|
||||
|
||||
$new_attachment->thread_id = $thread_id;
|
||||
|
||||
$new_attachment->save();
|
||||
@@ -353,4 +450,9 @@ class Attachment extends Model
|
||||
|
||||
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/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");
|
||||
}
|
||||
}
|
||||
@@ -54,5 +54,9 @@ class ClearCache extends Command
|
||||
if (!$this->option('doNotGenerateVars')) {
|
||||
$this->call('freescout:generate-vars');
|
||||
}
|
||||
// This should not be done during installation.
|
||||
if (\Helper::isInstalled()) {
|
||||
\Helper::queueWorkerRestart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -66,7 +66,7 @@ class GenerateVars extends Command
|
||||
}
|
||||
$this->info("Created: ".substr($file_path, strlen(base_path())+1));
|
||||
} catch (\Exception $e) {
|
||||
$msg = "Error occured saving /storage/app/public/js/vars.js. ".\Helper::formatException($e);
|
||||
$msg = "Error occurred saving /storage/app/public/js/vars.js. ".\Helper::formatException($e);
|
||||
\Log::error($msg);
|
||||
$this->error($msg);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
169
app/Console/Commands/ModuleUpdate.php
Normal file
169
app/Console/Commands/ModuleUpdate.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update custom modules.
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
109
app/Console/Commands/ParseEml.php
Normal file
109
app/Console/Commands/ParseEml.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?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('Reply-To: ');
|
||||
$this->info(json_encode($message->getReplyTo()[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('Date: ');
|
||||
$this->info($message->getDate());
|
||||
$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,11 +49,20 @@ 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')
|
||||
@@ -57,6 +72,12 @@ class Kernel extends ConsoleKernel
|
||||
$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) {
|
||||
@@ -82,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')) {
|
||||
@@ -107,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"
|
||||
@@ -138,13 +225,25 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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', $queue_work_params)
|
||||
->everyMinute()
|
||||
->withoutOverlapping()
|
||||
@@ -152,26 +251,16 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '".\Helper::getWorkerIdentifier()."'"));
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
312
app/Customer.php
312
app/Customer.php
@@ -3,6 +3,7 @@
|
||||
namespace App;
|
||||
|
||||
use App\Email;
|
||||
use App\CustomerChannel;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -15,7 +16,6 @@ class Customer extends Model
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
const PHOTO_DIRECTORY = 'customers';
|
||||
const PHOTO_SIZE = 64; // px
|
||||
const PHOTO_QUALITY = 77;
|
||||
|
||||
/**
|
||||
@@ -50,10 +50,10 @@ class Customer extends Model
|
||||
public static $phone_types = [
|
||||
self::PHONE_TYPE_WORK => 'work',
|
||||
self::PHONE_TYPE_HOME => 'home',
|
||||
self::PHONE_TYPE_OTHER => 'other',
|
||||
self::PHONE_TYPE_MOBILE => 'mobile',
|
||||
self::PHONE_TYPE_FAX => 'fax',
|
||||
self::PHONE_TYPE_PAGER => 'pager',
|
||||
self::PHONE_TYPE_OTHER => 'other',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -130,6 +130,7 @@ class Customer extends Model
|
||||
*/
|
||||
const SOCIAL_TYPE_TWITTER = 1;
|
||||
const SOCIAL_TYPE_FACEBOOK = 2;
|
||||
const SOCIAL_TYPE_TELEGRAM = 14;
|
||||
const SOCIAL_TYPE_LINKEDIN = 3;
|
||||
const SOCIAL_TYPE_ABOUTME = 4;
|
||||
const SOCIAL_TYPE_GOOGLE = 5;
|
||||
@@ -145,6 +146,7 @@ class Customer extends Model
|
||||
public static $social_types = [
|
||||
self::SOCIAL_TYPE_TWITTER => 'twitter',
|
||||
self::SOCIAL_TYPE_FACEBOOK => 'facebook',
|
||||
self::SOCIAL_TYPE_TELEGRAM => 'telegram',
|
||||
self::SOCIAL_TYPE_LINKEDIN => 'linkedin',
|
||||
self::SOCIAL_TYPE_ABOUTME => 'aboutme',
|
||||
self::SOCIAL_TYPE_GOOGLE => 'google',
|
||||
@@ -161,6 +163,7 @@ class Customer extends Model
|
||||
public static $social_type_names = [
|
||||
self::SOCIAL_TYPE_TWITTER => 'Twitter',
|
||||
self::SOCIAL_TYPE_FACEBOOK => 'Facebook',
|
||||
self::SOCIAL_TYPE_TELEGRAM => 'Telegram',
|
||||
self::SOCIAL_TYPE_LINKEDIN => 'Linkedin',
|
||||
self::SOCIAL_TYPE_ABOUTME => 'About.me',
|
||||
self::SOCIAL_TYPE_GOOGLE => 'Google',
|
||||
@@ -487,7 +490,7 @@ class Customer extends Model
|
||||
*/
|
||||
public function getMainEmail()
|
||||
{
|
||||
return optional($this->emails_cached()->first())->email;
|
||||
return optional($this->emails_cached()->first())->email.'';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -593,12 +596,19 @@ class Customer extends Model
|
||||
$this->emails()->saveMany($new_emails);
|
||||
}
|
||||
}
|
||||
// Create customers for deleted emails
|
||||
|
||||
foreach ($deleted_emails as $email) {
|
||||
$customer = new self();
|
||||
$customer->save();
|
||||
$email->customer()->associate($customer);
|
||||
$email->save();
|
||||
if (Conversation::where('customer_email', $email->email)->exists()) {
|
||||
// Create customers for deleted emails
|
||||
// if there is a conversation with 'customer_email'.
|
||||
$customer = new self();
|
||||
$customer->save();
|
||||
$email->customer()->associate($customer);
|
||||
$email->save();
|
||||
} else {
|
||||
// Simply delete an email.
|
||||
$email->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -626,20 +636,31 @@ class Customer extends Model
|
||||
*/
|
||||
public function getPhones($dummy_if_empty = false)
|
||||
{
|
||||
$phones = json_decode($this->phones, true);
|
||||
$phones = json_decode($this->phones ?? '', true);
|
||||
|
||||
if (is_array($phones) && count($phones)) {
|
||||
return $phones;
|
||||
} elseif ($dummy_if_empty) {
|
||||
return [[
|
||||
'type' => self::PHONE_TYPE_WORK,
|
||||
'value' => '',
|
||||
'type' => self::PHONE_TYPE_WORK,
|
||||
]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function getMainPhoneValue()
|
||||
{
|
||||
return $this->getMainPhoneNumber();
|
||||
}
|
||||
|
||||
public function getMainPhoneNumber()
|
||||
{
|
||||
$phones = $this->getPhones();
|
||||
return $phones[0]['value'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set phones as JSON.
|
||||
*
|
||||
@@ -682,12 +703,14 @@ class Customer extends Model
|
||||
$phones[] = [
|
||||
'value' => (string) $phone['value'],
|
||||
'type' => (int) $phone['type'],
|
||||
'n' => (string)\Helper::phoneToNumeric($phone['value']),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$phones[] = [
|
||||
'value' => (string) $phone,
|
||||
'type' => self::PHONE_TYPE_WORK
|
||||
'type' => self::PHONE_TYPE_WORK,
|
||||
'n' => (string)\Helper::phoneToNumeric($phone),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -718,8 +741,16 @@ class Customer extends Model
|
||||
*/
|
||||
public static function findByPhone($phone)
|
||||
{
|
||||
$phone = trim($phone);
|
||||
return Customer::where('phones', 'LIKE', '%"'.$phone.'"%')->first();
|
||||
return Customer::byPhone($phone)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query.
|
||||
*/
|
||||
public static function byPhone($phone)
|
||||
{
|
||||
$phone_numeric = \Helper::phoneToNumeric($phone);
|
||||
return Customer::where('phones', 'LIKE', '%"'.$phone_numeric.'"%');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -729,7 +760,7 @@ class Customer extends Model
|
||||
*/
|
||||
public function getSocialProfiles($dummy_if_empty = false)
|
||||
{
|
||||
$social_profiles = json_decode($this->social_profiles, true);
|
||||
$social_profiles = json_decode($this->social_profiles ?? '', true);
|
||||
|
||||
if (is_array($social_profiles) && count($social_profiles)) {
|
||||
return json_decode($this->social_profiles, true);
|
||||
@@ -750,7 +781,7 @@ class Customer extends Model
|
||||
*/
|
||||
public function getWebsites($dummy_if_empty = false)
|
||||
{
|
||||
$websites = json_decode($this->websites, true);
|
||||
$websites = json_decode($this->websites ?? '', true);
|
||||
if (is_array($websites) && count($websites)) {
|
||||
return $websites;
|
||||
} elseif ($dummy_if_empty) {
|
||||
@@ -760,6 +791,12 @@ class Customer extends Model
|
||||
}
|
||||
}
|
||||
|
||||
public function getMainWebsite()
|
||||
{
|
||||
$websites = $this->getWebsites();
|
||||
return $websites[0] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set websites as JSON.
|
||||
*
|
||||
@@ -830,12 +867,16 @@ class Customer extends Model
|
||||
}
|
||||
|
||||
$social_profiles[] = [
|
||||
// Order of elements in array is important as we rely on it
|
||||
// when searching customers by social profiles using "like".
|
||||
'value' => (string) $social_profile['value'],
|
||||
'type' => $type,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$social_profiles[] = [
|
||||
// Order of elements in array is important as we rely on it
|
||||
// when searching customers by social profiles using "like".
|
||||
'value' => (string) $social_profile,
|
||||
'type' => self::SOCIAL_TYPE_OTHER,
|
||||
];
|
||||
@@ -914,9 +955,25 @@ class Customer extends Model
|
||||
$customer->save();
|
||||
}
|
||||
|
||||
if (empty($email_obj->id) || !$email_obj->customer_id) {
|
||||
$email_obj->customer()->associate($customer);
|
||||
$email_obj->save();
|
||||
if (empty($email_obj->id) || !$email_obj->customer_id || $email_obj->customer_id != $customer->id) {
|
||||
// Email may have been set in setData().
|
||||
$save_email = true;
|
||||
if (!empty($data['emails']) && is_array($data['emails'])) {
|
||||
foreach ($data['emails'] as $data_email) {
|
||||
if (is_string($data_email) && $data_email == $email) {
|
||||
$save_email = false;
|
||||
break;
|
||||
}
|
||||
if (is_array($data_email) && !empty($data_email['value']) && $data_email['value'] == $email) {
|
||||
$save_email = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($save_email) {
|
||||
$email_obj->customer()->associate($customer);
|
||||
$email_obj->save();
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: check phone uniqueness.
|
||||
@@ -946,10 +1003,24 @@ class Customer extends Model
|
||||
|
||||
if ($replace_data) {
|
||||
// Replace data.
|
||||
$this->fill($data);
|
||||
$data_prepared = $data;
|
||||
foreach ($data_prepared as $i => $value) {
|
||||
if (is_array($value)) {
|
||||
unset($data_prepared[$i]);
|
||||
}
|
||||
}
|
||||
$this->fill($data_prepared);
|
||||
$result = true;
|
||||
} else {
|
||||
// Update empty fields.
|
||||
|
||||
// Do not set last name if first name is already set (and vise versa).
|
||||
if (!empty($this->first_name) && !empty($data['last_name'])) {
|
||||
unset($data['last_name']);
|
||||
}
|
||||
if (!empty($this->last_name) && !empty($data['first_name'])) {
|
||||
unset($data['first_name']);
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $this->fillable) && empty($this->$key)) {
|
||||
$this->$key = $value;
|
||||
@@ -966,20 +1037,6 @@ class Customer extends Model
|
||||
if (!in_array($key, $this->json_fields) && $key != 'emails') {
|
||||
continue;
|
||||
}
|
||||
if ($key == 'emails') {
|
||||
foreach ($value as $email_data) {
|
||||
if (!empty($email_data['value'])) {
|
||||
if (!$this->id) {
|
||||
$this->save();
|
||||
}
|
||||
$email_created = Email::create($email_data['value'], $this->id, $email_data['type']);
|
||||
|
||||
if ($email_created) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($key == 'phones') {
|
||||
if (isset($value['value'])) {
|
||||
$this->addPhone($value);
|
||||
@@ -1006,8 +1063,42 @@ class Customer extends Model
|
||||
$this->setSocialProfiles($value);
|
||||
$result = true;
|
||||
}
|
||||
if ($key == 'country') {
|
||||
if (array_search($this->country, Customer::$countries)) {
|
||||
$this->country = array_search($this->country, Customer::$countries);
|
||||
}
|
||||
$this->country = strtoupper(mb_substr($this->country, 0, 2));
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Emails must be processed the last as they need to save object.
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key == 'emails') {
|
||||
foreach ($value as $email_data) {
|
||||
if (is_string($email_data)) {
|
||||
if (!$this->id) {
|
||||
$this->save();
|
||||
}
|
||||
$email_created = Email::create($email_data, $this->id, Email::TYPE_WORK);
|
||||
|
||||
if ($email_created) {
|
||||
$result = true;
|
||||
}
|
||||
} elseif (!empty($email_data['value'])) {
|
||||
if (!$this->id) {
|
||||
$this->save();
|
||||
}
|
||||
$email_created = Email::create($email_data['value'], $this->id, $email_data['type']);
|
||||
|
||||
if ($email_created) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Maybe Todo: check phone uniqueness.
|
||||
// Same phone can be written in many ways, so it's almost useless to chek uniqueness.
|
||||
|
||||
@@ -1123,14 +1214,19 @@ class Customer extends Model
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
foreach ($data as $values) {
|
||||
foreach ($list as $email) {
|
||||
// Dummy customer.
|
||||
$customer = new Customer();
|
||||
$customer->email = $values['email'];
|
||||
$customer->first_name = $values['first_name'];
|
||||
$customer->last_name = $values['last_name'];
|
||||
$customer->email = $email;
|
||||
|
||||
$result[$values['email']] = $customer->getNameAndEmail();
|
||||
foreach ($data as $values) {
|
||||
if (strtolower($values['email']) == strtolower($email)) {
|
||||
$customer->first_name = $values['first_name'];
|
||||
$customer->last_name = $values['last_name'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$result[$email] = $customer->getNameAndEmail();
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -1174,7 +1270,7 @@ class Customer extends Model
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return asset('/img/default-avatar.png');
|
||||
return \Eventy::filter('customer.default_avatar', asset('/img/default-avatar.png'), $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1188,7 +1284,8 @@ class Customer extends Model
|
||||
*/
|
||||
public function savePhoto($real_path, $mime_type)
|
||||
{
|
||||
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, self::PHOTO_SIZE, self::PHOTO_SIZE);
|
||||
$photo_size = config('app.customer_photo_size');
|
||||
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, $photo_size, $photo_size);
|
||||
|
||||
if (!$resized_image) {
|
||||
return false;
|
||||
@@ -1199,7 +1296,7 @@ class Customer extends Model
|
||||
|
||||
$dest_dir = pathinfo($dest_path, PATHINFO_DIRNAME);
|
||||
if (!file_exists($dest_dir)) {
|
||||
\File::makeDirectory($dest_dir, 0755);
|
||||
\File::makeDirectory($dest_dir, \Helper::DIR_PERMISSIONS);
|
||||
}
|
||||
|
||||
// Remove current photo
|
||||
@@ -1263,7 +1360,15 @@ class Customer extends Model
|
||||
$sp['value_url'] = $sp['value'];
|
||||
|
||||
if (!preg_match("/^https?:\/\//i", $sp['value_url'])) {
|
||||
$sp['value_url'] = 'http://'.$sp['value_url'];
|
||||
switch ($sp['type']) {
|
||||
case self::SOCIAL_TYPE_TELEGRAM:
|
||||
$sp['value_url'] = 'https://t.me/'.$sp['value'];
|
||||
break;
|
||||
|
||||
default:
|
||||
$sp['value_url'] = 'http://'.$sp['value_url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (empty($sp['value_url'])) {
|
||||
$sp['value_url'] = '';
|
||||
@@ -1280,13 +1385,13 @@ class Customer extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
$image_data = file_get_contents($url);
|
||||
$image_data = \Helper::getRemoteFileContents($url);
|
||||
|
||||
if (!$image_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$temp_file = tempnam(sys_get_temp_dir(), 'photo');
|
||||
$temp_file = \Helper::getTempFileName();
|
||||
|
||||
\File::put($temp_file, $image_data);
|
||||
|
||||
@@ -1326,4 +1431,123 @@ class Customer extends Model
|
||||
$meta[$key] = $value;
|
||||
$this->meta = $meta;
|
||||
}
|
||||
|
||||
public static function getPhoneTypeName($code)
|
||||
{
|
||||
$phone_types = [
|
||||
self::PHONE_TYPE_WORK => __('Work'),
|
||||
self::PHONE_TYPE_HOME => __('Home'),
|
||||
self::PHONE_TYPE_OTHER => __('Other'),
|
||||
self::PHONE_TYPE_MOBILE => __('Mobile'),
|
||||
self::PHONE_TYPE_FAX => __('Fax'),
|
||||
self::PHONE_TYPE_PAGER => __('Pager'),
|
||||
];
|
||||
|
||||
return $phone_types[$code] ?? '';
|
||||
}
|
||||
|
||||
public static function isDefaultPhoneType($code)
|
||||
{
|
||||
return (self::PHONE_TYPE_WORK == $code);
|
||||
}
|
||||
|
||||
// Method does not check if the customer
|
||||
// has conversations.
|
||||
public function deleteCustomer()
|
||||
{
|
||||
// Delete emails.
|
||||
Email::where('customer_id', $this->id)->delete();
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
public function getChannels()
|
||||
{
|
||||
if (!$this->channel || !$this->channel_id) {
|
||||
return collect([]);
|
||||
}
|
||||
return CustomerChannel::where('customer_id', $this->id)->get();
|
||||
}
|
||||
|
||||
public function addChannel($channel, $channel_id)
|
||||
{
|
||||
// We are doing this to let existing modules not to throw error
|
||||
// and as a flag that this customer has record(s) in cucstomer_channel table.
|
||||
if (!$this->channel || !$this->channel_id) {
|
||||
$this->channel = $channel;
|
||||
$this->channel_id = $channel_id;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return CustomerChannel::create($this->id, $channel, $channel_id);
|
||||
}
|
||||
|
||||
public static function getCustomerByChannel($channel, $channel_id)
|
||||
{
|
||||
$customer_channel = CustomerChannel::where('channel', $channel)
|
||||
->where('channel_id', $channel_id)
|
||||
->first();
|
||||
|
||||
if ($customer_channel) {
|
||||
return $customer_channel->customer;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getChannelId($channel)
|
||||
{
|
||||
return CustomerChannel::where('customer_id', $this->id)
|
||||
->where('channel', $channel)
|
||||
->value('channel_id');
|
||||
}
|
||||
|
||||
public static function findCustomersBySocialProfile($type, $value, $exclude_channel = null)
|
||||
{
|
||||
$value = mb_strtolower($value);
|
||||
|
||||
$like = '%'.$value.'","type":'.$type.'}]';
|
||||
$customers = Customer::where('social_profiles', \Helper::sqlLikeOperator(), $like)->get();
|
||||
|
||||
// Now more prcise filtering.
|
||||
foreach ($customers as $i => $customer) {
|
||||
$ok = false;
|
||||
foreach ($customer->getSocialProfiles() as $social_profile) {
|
||||
if ($social_profile['type'] == $type
|
||||
// Try to check username written in different ways:
|
||||
// - username
|
||||
// - @username
|
||||
// - https://example.org/username
|
||||
&& preg_match("#(^|/|@)".preg_quote($value)."$#", trim(mb_strtolower($social_profile['value'])))
|
||||
) {
|
||||
$ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$ok) {
|
||||
$customers->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
if ($exclude_channel && count($customers)) {
|
||||
$exclude_customer_ids = CustomerChannel::whereIn('customer_id', $customers->pluck('id'))
|
||||
->where('channel', $exclude_channel)
|
||||
->pluck('customer_id');
|
||||
return $customers->whereNotIn('customer_id', $exclude_customer_ids);
|
||||
} else {
|
||||
return $customers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dummy customer.
|
||||
*/
|
||||
public static function getDummyCustomer()
|
||||
{
|
||||
$customer = new self();
|
||||
$customer->first_name = __('Customer');
|
||||
$customer->last_name = '';
|
||||
|
||||
return $customer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ 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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
119
app/Folder.php
119
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',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -152,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:
|
||||
@@ -161,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']];
|
||||
@@ -175,6 +175,26 @@ class Folder extends Model
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -202,6 +222,15 @@ 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;
|
||||
@@ -209,8 +238,8 @@ class Folder extends Model
|
||||
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)
|
||||
@@ -273,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) {
|
||||
@@ -350,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
1218
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
1218
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@@ -77,12 +77,12 @@ class CustomersController extends Controller
|
||||
// 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());
|
||||
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath() ?: $request->file('photo_url')->getPathname(), $request->file('photo_url')->getMimeType());
|
||||
|
||||
if ($path_url) {
|
||||
$customer->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.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -131,7 +131,7 @@ class CustomersController extends Controller
|
||||
'a_end' => '</a></strong>',
|
||||
]).' ';
|
||||
|
||||
$new_emails_change_customer[] = $email->email;
|
||||
$new_emails_change_customer[] = $email;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,17 +157,22 @@ class CustomersController extends Controller
|
||||
|
||||
$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) {
|
||||
@@ -212,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'));
|
||||
|
||||
@@ -285,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);
|
||||
@@ -306,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();
|
||||
@@ -328,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'][] = [
|
||||
@@ -401,7 +412,7 @@ class CustomersController extends Controller
|
||||
}
|
||||
|
||||
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||
$response['msg'] = 'Unknown error occured';
|
||||
$response['msg'] = 'Unknown error occurred';
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
|
||||
332
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
332
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
@@ -63,6 +63,8 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$this->authorize('create', 'App\Mailbox');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
@@ -72,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();
|
||||
@@ -80,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'));
|
||||
@@ -101,7 +109,12 @@ class MailboxesController extends Controller
|
||||
$accessible_route = '';
|
||||
|
||||
$mailbox_settings = $user->mailboxSettings($mailbox->id);
|
||||
$access_permissions = json_decode($mailbox_settings->access);
|
||||
|
||||
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) {
|
||||
@@ -142,6 +155,7 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function updateSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$user = auth()->user();
|
||||
@@ -152,6 +166,11 @@ class MailboxesController extends Controller
|
||||
|
||||
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([
|
||||
@@ -163,7 +182,7 @@ class MailboxesController extends Controller
|
||||
$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',
|
||||
'aliases' => 'nullable|string',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
@@ -172,8 +191,14 @@ class MailboxesController extends Controller
|
||||
]);
|
||||
|
||||
//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.'));
|
||||
}
|
||||
|
||||
if ($validator->fails()) {
|
||||
$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();
|
||||
@@ -192,7 +217,10 @@ class MailboxesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('mailbox.settings_before_save', $mailbox, $request);
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
$mailbox->signature = \Helper::stripDangerousTags($mailbox->signature);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
@@ -242,7 +270,7 @@ class MailboxesController extends Controller
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$mailbox->users()->sync($request->users);
|
||||
$mailbox->users()->sync(\Eventy::filter('mailbox.permission_users', $request->users, $id) ?: []);
|
||||
$mailbox->syncPersonalFolders($request->users);
|
||||
|
||||
// Save admins settings.
|
||||
@@ -264,7 +292,7 @@ class MailboxesController extends Controller
|
||||
$access = [];
|
||||
$mailbox_with_settings = $mailbox_user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
|
||||
foreach (\App\Mailbox::$access_permissions as $perm) {
|
||||
foreach (Mailbox::$access_permissions as $perm) {
|
||||
if (!empty($request->managers[$mailbox_user->id]['access'][$perm])) {
|
||||
$access[] = $request->managers[$mailbox_user->id]['access'][$perm];
|
||||
}
|
||||
@@ -320,7 +348,7 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
// Do not save dummy password.
|
||||
if (preg_match("/^\*+$/", $request->out_password)) {
|
||||
if (preg_match("/^\*+$/", $request->out_password ?? '')) {
|
||||
$params = $request->except(['out_password']);
|
||||
} else {
|
||||
$params = $request->all();
|
||||
@@ -333,7 +361,7 @@ 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!'));
|
||||
|
||||
@@ -343,16 +371,16 @@ class MailboxesController extends Controller
|
||||
/**
|
||||
* Mailbox incoming settings.
|
||||
*/
|
||||
public function connectionIncoming($id)
|
||||
public function connectionIncoming($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
$fields = [
|
||||
'in_server' => $mailbox->in_server,
|
||||
'in_port' => $mailbox->in_port,
|
||||
'in_username' => $mailbox->in_username,
|
||||
'in_password' => $mailbox->in_password,
|
||||
'in_server' => $mailbox->in_server ?? '',
|
||||
'in_port' => $mailbox->in_port ?? '',
|
||||
'in_username' => $mailbox->in_username ?? '',
|
||||
'in_password' => $mailbox->in_password ?? '',
|
||||
];
|
||||
|
||||
$validator = Validator::make($fields, [
|
||||
@@ -392,12 +420,14 @@ class MailboxesController extends Controller
|
||||
]);
|
||||
|
||||
// Do not save dummy password.
|
||||
if (preg_match("/^\*+$/", $request->in_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.
|
||||
@@ -420,15 +450,11 @@ class MailboxesController extends Controller
|
||||
/**
|
||||
* View mailbox.
|
||||
*/
|
||||
public function view($id, $folder_id = null)
|
||||
public function view(Request $request, $id, $folder_id = null)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user->isAdmin()) {
|
||||
$mailbox = Mailbox::findOrFailWithSettings($id, $user->id);
|
||||
} else {
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
}
|
||||
$mailbox = Mailbox::findOrFailWithSettings($id, $user->id);
|
||||
$this->authorize('viewCached', $mailbox);
|
||||
|
||||
$folders = $mailbox->getAssesibleFolders();
|
||||
@@ -449,7 +475,9 @@ class MailboxesController extends Controller
|
||||
$this->authorize('view', $folder);
|
||||
|
||||
$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,
|
||||
@@ -534,6 +562,7 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
$mailbox->auto_reply_message = \Helper::stripDangerousTags($mailbox->auto_reply_message);
|
||||
|
||||
$mailbox->save();
|
||||
|
||||
@@ -595,17 +624,22 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
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 occurred 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']) {
|
||||
@@ -628,29 +662,34 @@ class MailboxesController extends Controller
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
$response = \Eventy::filter('mailbox.fetch_test', $response, $mailbox);
|
||||
|
||||
$tested = (isset($response['tested']) && $response['tested'] === true);
|
||||
|
||||
// Check if outgoing port is open.
|
||||
if (!$response['msg']) {
|
||||
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']) {
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$test_result = false;
|
||||
|
||||
try {
|
||||
$test_result = \MailHelper::fetchTest($mailbox);
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = $e->getMessage();
|
||||
}
|
||||
$test_result = \MailHelper::fetchTest($mailbox);
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['log'] = $test_result['log'] ?? '';
|
||||
|
||||
if ($test_result['result'] != 'success' && $test_result['msg']) {
|
||||
$response['msg'] = $test_result['msg'];
|
||||
}
|
||||
if ($test_result['result'] != 'success' && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurred connecting to the server');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
@@ -675,23 +714,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');
|
||||
@@ -715,15 +764,20 @@ class MailboxesController extends Controller
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} 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();
|
||||
@@ -743,8 +797,6 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->isAdmin()) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
@@ -752,7 +804,7 @@ class MailboxesController extends Controller
|
||||
$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($id);
|
||||
$user->mailboxes()->attach($mailbox->id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
}
|
||||
$mailbox_user->settings->mute = (bool)$request->mute;
|
||||
@@ -768,10 +820,176 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
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 ?? '';
|
||||
$in_out = $request->in_out ?? 'in';
|
||||
|
||||
$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'];
|
||||
}
|
||||
if (!empty($state_data['in_out'])) {
|
||||
$in_out = $state_data['in_out'];
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ($in_out == 'in') {
|
||||
$username = $mailbox->in_username;
|
||||
$password = $mailbox->in_password;
|
||||
} else {
|
||||
$username = $mailbox->out_username;
|
||||
$password = $mailbox->out_password;
|
||||
}
|
||||
if (empty($username)) {
|
||||
return 'Enter oAuth Client ID as Username and save mailbox settings';
|
||||
}
|
||||
if (empty($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)) {
|
||||
// Start.
|
||||
$state = [
|
||||
'provider' => $provider,
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'in_out' => $in_out,
|
||||
'state' => crc32($username.$password),
|
||||
];
|
||||
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'state' => json_encode($state),
|
||||
'client_id' => $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 {
|
||||
// state is set.
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $username,
|
||||
'client_secret' => $password,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
// Set username and password for the oppozite in_out.
|
||||
if ($in_out == 'in') {
|
||||
if (empty($mailbox->out_server)
|
||||
|| (trim($mailbox->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP
|
||||
&& (!$mailbox->out_username || $mailbox->out_username == $username))
|
||||
) {
|
||||
$mailbox->out_username = $username;
|
||||
$mailbox->out_password = $password;
|
||||
}
|
||||
//$mailbox->out_method = Mailbox::OUT_METHOD_SMTP;
|
||||
} else {
|
||||
$mailbox->in_username = $username;
|
||||
$mailbox->in_password = $password;
|
||||
}
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
return __('Error occurred').': '.htmlspecialchars($token_data['error']);
|
||||
}
|
||||
|
||||
if ($in_out == 'in') {
|
||||
$route = 'mailboxes.connection.incoming';
|
||||
} else {
|
||||
$route = 'mailboxes.connection';
|
||||
}
|
||||
return redirect()->route($route, ['id' => $mailbox_id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function oauthDisconnect(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
$in_out = $request->in_out ?? 'in';
|
||||
|
||||
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
// oAuth Disconnect.
|
||||
$mailbox->removeMetaParam('oauth', true);
|
||||
|
||||
if ($in_out == 'in') {
|
||||
$route = 'mailboxes.connection.incoming';
|
||||
} else {
|
||||
$route = 'mailboxes.connection';
|
||||
}
|
||||
|
||||
return \MailHelper::oauthDisconnect($provider, route($route, ['id' => $mailbox_id]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +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');
|
||||
}
|
||||
|
||||
@@ -54,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(),
|
||||
@@ -67,14 +71,19 @@ class ModulesController extends Controller
|
||||
'requiredPhpExtensions' => $module->get('requiredPhpExtensions'),
|
||||
'requiredPhpExtensionsMissing' => \App\Module::getMissingExtensions($module->get('requiredPhpExtensions')),
|
||||
'requiredModulesMissing' => \App\Module::getMissingModules($module->get('requiredModules'), $modules),
|
||||
'img' => $img,
|
||||
'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
|
||||
@@ -86,6 +95,9 @@ 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]);
|
||||
@@ -118,17 +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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -221,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
|
||||
@@ -234,9 +305,9 @@ class ModulesController extends Controller
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
} elseif (!empty($result['error'])) {
|
||||
$response['msg'] = $this->getErrorMessage($result['error'], $result);
|
||||
$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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,9 +350,25 @@ class ModulesController extends Controller
|
||||
case 'inactive':
|
||||
$response['msg'] = __('License key has not been activated yet');
|
||||
case 'site_inactive':
|
||||
$response['msg'] = __('Your domain is deactivated');
|
||||
$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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,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 = '';
|
||||
@@ -318,10 +405,10 @@ class ModulesController extends Controller
|
||||
|
||||
// Check public folder.
|
||||
if ($module && file_exists($module->getPath().DIRECTORY_SEPARATOR.'Public')) {
|
||||
$public_folder = public_path().\Module::getPublicPath($alias);
|
||||
if (!file_exists($public_folder)) {
|
||||
$symlink_path = public_path().\Module::getPublicPath($alias);
|
||||
if (!file_exists($symlink_path)) {
|
||||
$type = 'danger';
|
||||
$msg = 'Error occured creating a module symlink ('.$public_folder.'). Please check folder permissions.';
|
||||
$msg = 'Error occurred creating a module symlink ('.$symlink_path.'). Please check folder permissions.';
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
@@ -329,7 +416,7 @@ class ModulesController extends Controller
|
||||
|
||||
if ($type == 'success') {
|
||||
// Migrate again, in case migration did not work in the moment the module was activated.
|
||||
\Artisan::call('migrate');
|
||||
\Artisan::call('migrate', ['--force' => true]);
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
@@ -360,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
|
||||
@@ -399,7 +486,7 @@ class ModulesController extends Controller
|
||||
} else {
|
||||
if (!empty($result['status']) && $result['status'] == 'success') {
|
||||
$db_module = \App\Module::getByAlias($alias);
|
||||
if ($db_module && trim($db_module->license) == trim($license)) {
|
||||
if ($db_module && trim($db_module->license ?? '') == trim($license ?? '')) {
|
||||
// Remove remembered license key and deactivate license in DB
|
||||
\App\Module::deactivateLicense($alias, '');
|
||||
|
||||
@@ -410,7 +497,7 @@ class ModulesController extends Controller
|
||||
|
||||
// Flash does not work here.
|
||||
$flash = [
|
||||
'text' => '<strong>'.__('License successfully DEactivated!').'</strong>',
|
||||
'text' => '<strong>'.__('License successfully Deactivated!').'</strong>',
|
||||
'unescaped' => true,
|
||||
'type' => 'success',
|
||||
];
|
||||
@@ -418,9 +505,9 @@ class ModulesController extends Controller
|
||||
|
||||
$response['status'] = 'success';
|
||||
} elseif (!empty($result['error'])) {
|
||||
$response['msg'] = $this->getErrorMessage($result['error'], $result);
|
||||
$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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,110 +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>']));
|
||||
}
|
||||
} elseif ($license_details['status'] && $response['msg'] = $this->getErrorMessage($license_details['status'])) {
|
||||
//$response['msg'] = ;
|
||||
} 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,
|
||||
];
|
||||
@@ -558,51 +569,50 @@ 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);
|
||||
}
|
||||
|
||||
public 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');
|
||||
break;
|
||||
case 'expired':
|
||||
$msg = __('License key has expired');
|
||||
break;
|
||||
case 'key_mismatch':
|
||||
$msg = __('License key belongs to another module');
|
||||
break;
|
||||
case 'invalid_item_id':
|
||||
$msg = __('Module not found in the modules directory');
|
||||
break;
|
||||
default:
|
||||
if ($result && !empty($result['error'])) {
|
||||
$msg = __('Error code:'.' '.$result['error']);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,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.
|
||||
@@ -34,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]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,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.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -97,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
|
||||
@@ -124,7 +129,7 @@ class PublicController extends Controller
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -173,6 +178,20 @@ class PublicController extends Controller
|
||||
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.
|
||||
@@ -198,4 +217,12 @@ class PublicController extends Controller
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Needed for the mobile app.
|
||||
*/
|
||||
// public function mobilePing()
|
||||
// {
|
||||
// echo file_get_contents(public_path('installer/css/fontawesome.css'));
|
||||
// }
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class SecureController extends Controller
|
||||
}
|
||||
|
||||
// Sort by name.
|
||||
$mailboxes = $mailboxes->sortBy('name');
|
||||
$mailboxes = \Eventy::filter('dashboard.mailboxes', $mailboxes->sortBy('name'));
|
||||
|
||||
return view('secure/dashboard', ['mailboxes' => $mailboxes]);
|
||||
}
|
||||
@@ -112,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 = '';
|
||||
@@ -132,13 +136,16 @@ class SecureController extends Controller
|
||||
$status .= '. Message-ID: '.$record->message_id;
|
||||
}
|
||||
}
|
||||
if ($record->smtp_queue_id) {
|
||||
$status .= '. SMTP ID: '.$record->smtp_queue_id;
|
||||
}
|
||||
|
||||
$logs[] = [
|
||||
'date' => $record->created_at,
|
||||
'type' => $record->getMailTypeName(),
|
||||
'email' => $record->email,
|
||||
'status' => $status,
|
||||
'conversation' => $conversation,
|
||||
'message' => $conversation,
|
||||
'user' => $record->user,
|
||||
'customer' => $record->customer,
|
||||
];
|
||||
@@ -170,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;
|
||||
@@ -208,7 +215,7 @@ class SecureController extends Controller
|
||||
}
|
||||
|
||||
if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) {
|
||||
$response['msg'] = __('Error occured uploading file');
|
||||
$response['msg'] = __('Error occurred uploading file');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
@@ -220,7 +227,7 @@ class SecureController extends Controller
|
||||
$response['status'] = 'success';
|
||||
$response['url'] = Helper::uploadedFileUrl($filename);
|
||||
} else {
|
||||
$response['msg'] = __('Error occured uploading file');
|
||||
$response['msg'] = __('Error occurred uploading file');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -100,14 +101,24 @@ class SettingsController extends Controller
|
||||
'env' => 'APP_FETCH_SCHEDULE',
|
||||
],
|
||||
'mail_password' => [
|
||||
'safe_password' => true
|
||||
]
|
||||
'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',
|
||||
],
|
||||
@@ -128,9 +139,19 @@ class SettingsController extends Controller
|
||||
];
|
||||
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' => [
|
||||
@@ -155,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];
|
||||
@@ -175,10 +198,12 @@ 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,
|
||||
'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),
|
||||
@@ -193,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':
|
||||
@@ -206,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'),
|
||||
@@ -217,6 +244,8 @@ class SettingsController extends Controller
|
||||
break;
|
||||
}
|
||||
|
||||
$settings = \Eventy::filter('settings.alter_section_settings', $settings, $section);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -284,6 +313,10 @@ class SettingsController extends Controller
|
||||
$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);
|
||||
}
|
||||
@@ -296,8 +329,13 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -318,6 +356,8 @@ 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]);
|
||||
}
|
||||
@@ -354,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 occurred 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']) {
|
||||
@@ -383,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,6 +4,7 @@ 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;
|
||||
@@ -19,7 +20,9 @@ class SystemController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('auth', ['except' => [
|
||||
'cron'
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,28 +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 = [
|
||||
'shell_exec (PHP)' => function_exists('shell_exec'),
|
||||
'proc_open (PHP)' => function_exists('proc_open'),
|
||||
'ps (shell)' => function_exists('shell_exec') ? shell_exec('ps') : false,
|
||||
];
|
||||
$functions = \Helper::checkRequiredFunctions();
|
||||
|
||||
// Permissions.
|
||||
$permissions = [];
|
||||
@@ -67,8 +53,10 @@ class SystemController extends Controller
|
||||
// Check if cache files are writable.
|
||||
$non_writable_cache_file = '';
|
||||
if (function_exists('shell_exec')) {
|
||||
$non_writable_cache_file = shell_exec('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);
|
||||
$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 = '';
|
||||
}
|
||||
@@ -92,8 +80,8 @@ class SystemController extends Controller
|
||||
$env_is_writable = is_writable(base_path('.env'));
|
||||
|
||||
// Jobs
|
||||
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->get();
|
||||
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->get();
|
||||
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->limit(100)->get();
|
||||
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->limit(100)->get();
|
||||
$failed_queues = $failed_jobs->pluck('queue')->unique();
|
||||
|
||||
// Commands
|
||||
@@ -109,10 +97,16 @@ class SystemController extends Controller
|
||||
$running_commands = 0;
|
||||
|
||||
try {
|
||||
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
|
||||
$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];
|
||||
@@ -131,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',
|
||||
@@ -206,6 +200,11 @@ class SystemController extends Controller
|
||||
$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,
|
||||
@@ -220,6 +219,8 @@ class SystemController extends Controller
|
||||
'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(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -231,6 +232,12 @@ class SystemController extends Controller
|
||||
\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'));
|
||||
@@ -283,6 +290,7 @@ class SystemController extends Controller
|
||||
$params = [];
|
||||
$params['--days'] = (int)$request->days;
|
||||
$params['--unseen'] = (int)$request->unseen;
|
||||
$params['--debug'] = (int)$request->debug;
|
||||
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
|
||||
break;
|
||||
|
||||
@@ -324,7 +332,7 @@ class SystemController extends Controller
|
||||
|
||||
// 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);
|
||||
@@ -342,7 +350,7 @@ class SystemController extends Controller
|
||||
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured').': '.$e->getMessage();
|
||||
$response['msg'] = __('Error occurred').': '.$e->getMessage();
|
||||
}
|
||||
if (!$response['msg'] && !$response['new_version_available']) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
@@ -359,9 +367,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$this->authorize('create', 'App\User');
|
||||
$auth_user = auth()->user();
|
||||
|
||||
@@ -74,7 +75,12 @@ class UsersController extends Controller
|
||||
}
|
||||
$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();
|
||||
@@ -92,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
|
||||
@@ -103,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"'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +156,8 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function profileSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
$this->authorize('update', $user);
|
||||
|
||||
@@ -155,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',
|
||||
@@ -175,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.'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,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();
|
||||
@@ -214,12 +232,14 @@ class UsersController extends Controller
|
||||
$request_data['status'] = User::STATUS_ACTIVE;
|
||||
}
|
||||
}
|
||||
$user->fill($request_data);
|
||||
$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'));
|
||||
@@ -266,7 +286,7 @@ class UsersController extends Controller
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$user->mailboxes()->sync($request->mailboxes);
|
||||
$user->mailboxes()->sync($request->mailboxes ?: []);
|
||||
$user->syncPersonalFolders($request->mailboxes);
|
||||
|
||||
// Save permissions.
|
||||
@@ -370,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;
|
||||
@@ -478,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +572,7 @@ 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;
|
||||
|
||||
@@ -560,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);
|
||||
|
||||
@@ -41,6 +41,7 @@ class Kernel extends HttpKernel
|
||||
\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,
|
||||
],
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ 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);
|
||||
|
||||
|
||||
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);
|
||||
|
||||
@@ -14,7 +14,7 @@ class TerminateHandler
|
||||
|
||||
public function terminate($request, $response)
|
||||
{
|
||||
// Process events which occured
|
||||
// Process events which occurred
|
||||
Subscription::processEvents();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ class TokenAuth
|
||||
*/
|
||||
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, "'.config('app.key').'"))') , $request->auth_token)
|
||||
$user = User::where(\DB::raw('md5(CONCAT(id, created_at, "'.config('app.key').'"))'), $request->auth_token)
|
||||
->first();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '[TokenAuth]');
|
||||
|
||||
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,15 +51,20 @@ 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;
|
||||
|
||||
@@ -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 = 168; // One per hour
|
||||
|
||||
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,16 +55,18 @@ 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->id;
|
||||
});
|
||||
$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;
|
||||
@@ -75,12 +85,23 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
$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) {
|
||||
@@ -101,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(true, true);
|
||||
$from_name = '';
|
||||
if ($last_thread->customer) {
|
||||
$from_name = $last_thread->customer->getFullName(true, true);
|
||||
}
|
||||
if ($from_name) {
|
||||
$from_name = $from_name.' '.__('via').' '.$mailbox->name;
|
||||
}
|
||||
@@ -115,6 +139,7 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
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;
|
||||
|
||||
@@ -156,7 +181,8 @@ 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) {
|
||||
// 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);
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
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;
|
||||
@@ -17,9 +19,6 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
// Number of retries + 1
|
||||
public $tries = 168; // one per hour
|
||||
|
||||
public $conversation;
|
||||
|
||||
public $threads;
|
||||
@@ -32,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.
|
||||
*
|
||||
@@ -61,6 +71,11 @@ 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];
|
||||
@@ -68,10 +83,8 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
|
||||
// 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.
|
||||
@@ -87,13 +100,16 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
|
||||
// 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->id;
|
||||
});
|
||||
$this->threads = Thread::sortThreads($this->threads);
|
||||
|
||||
$new = false;
|
||||
$headers = [];
|
||||
|
||||
$this->last_thread = $this->threads->first();
|
||||
|
||||
if ($this->last_thread === null) {
|
||||
return;
|
||||
}
|
||||
$last_customer_thread = null;
|
||||
|
||||
// If thread is draft, it means it has been undone
|
||||
@@ -101,6 +117,17 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -115,9 +142,37 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -154,19 +209,34 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
|
||||
\MailHelper::setMailDriver($mailbox, $this->last_thread->created_by_user, $this->conversation);
|
||||
|
||||
// Get penultimate email Message-Id if reply
|
||||
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/3330
|
||||
if (!\MailHelper::$smtp_queue_id_plugin_registered) {
|
||||
\Mail::getSwiftMailer()->registerPlugin(new SwiftGetSmtpQueueId());
|
||||
\MailHelper::$smtp_queue_id_plugin_registered = true;
|
||||
}
|
||||
|
||||
$this->message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->last_thread->id.'-'.md5($this->last_thread->id).'@'.$mailbox->getEmailDomain();
|
||||
$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());
|
||||
@@ -203,15 +273,29 @@ 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);
|
||||
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox, $subject, $threads_count);
|
||||
|
||||
$smtp_queue_id = null;
|
||||
|
||||
try {
|
||||
Mail::to($to)
|
||||
->cc($cc_array)
|
||||
->bcc($bcc_array)
|
||||
->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
|
||||
if ($this->attempts() == 1) {
|
||||
@@ -235,6 +319,14 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
|
||||
$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 && !preg_match("/".config("app.no_retry_mail_errors")."/i", $error_message)) {
|
||||
@@ -246,6 +338,13 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
$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;
|
||||
@@ -259,40 +358,143 @@ 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();
|
||||
|
||||
$envelope['from'] = $mailbox->getMailFrom()['address'];
|
||||
$envelope['to'] = $this->customer_email;
|
||||
$envelope['subject'] = 'Re: ' . $this->conversation->subject;
|
||||
$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: <'.$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);
|
||||
}
|
||||
|
||||
$part1['type'] = TYPETEXT;
|
||||
$part1['subtype'] = 'html';
|
||||
$part1['contents.data'] = $reply_mail->render();
|
||||
$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 {
|
||||
$folder = $client->getFolder($imap_sent_folder);
|
||||
$folder->appendMessage(imap_mail_compose($envelope, [$part1]), '\Seen', now()->format('d-M-Y H:i:s O'));
|
||||
// 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, 'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
|
||||
\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, 'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
|
||||
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,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'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,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)) {
|
||||
@@ -349,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);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -27,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.
|
||||
@@ -44,6 +48,10 @@ class SendAutoReply
|
||||
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.
|
||||
|
||||
@@ -23,9 +23,11 @@ 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()) {
|
||||
return;
|
||||
if (!$conversation->customer->getMainEmail()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$replies = $conversation->getReplies();
|
||||
@@ -35,6 +37,18 @@ class SendReplyToCustomer
|
||||
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));
|
||||
@@ -44,8 +58,10 @@ class SendReplyToCustomer
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
//if (!$conversation->imported) {
|
||||
$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(now()->addSeconds(Conversation::UNDO_TIMOUT))
|
||||
->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,8 @@ 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');
|
||||
|
||||
@@ -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,6 +27,8 @@ class UserInvite extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
\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;
|
||||
|
||||
264
app/Mailbox.php
264
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;
|
||||
@@ -25,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.
|
||||
@@ -32,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.
|
||||
@@ -54,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 => '',
|
||||
@@ -78,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',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -98,6 +110,7 @@ class Mailbox extends Model
|
||||
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,
|
||||
@@ -135,7 +148,7 @@ class Mailbox extends Model
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'aliases', '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 $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()
|
||||
{
|
||||
@@ -171,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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,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])
|
||||
@@ -336,7 +383,7 @@ class Mailbox extends Model
|
||||
->orderBy('type')
|
||||
->get();
|
||||
|
||||
return \Eventy::filter('mailbox.folders', $folders);
|
||||
return \Eventy::filter('mailbox.folders', $folders, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,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 {
|
||||
@@ -416,10 +469,12 @@ class Mailbox extends Model
|
||||
$users = User::sortUsers($users);
|
||||
}
|
||||
|
||||
$users = \Eventy::filter('mailbox.users_having_access', $users, $this, $cache, $fields, $sort);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function usersAssignable($cache = true)
|
||||
public function usersAssignable($cache = true, $exclude_hidden = true)
|
||||
{
|
||||
// Exclude hidden admins.
|
||||
$mailbox_id = $this->id;
|
||||
@@ -432,11 +487,17 @@ class Mailbox extends Model
|
||||
->remember(\Helper::cacheTime($cache))
|
||||
->get();
|
||||
|
||||
$users = $this->users()->select('users.*')->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
|
||||
$users = $this->users()->select(['users.*', 'mailbox_user.hide'])
|
||||
->remember(\Helper::cacheTime($cache))
|
||||
->get()
|
||||
->merge($admins)
|
||||
->unique();
|
||||
|
||||
foreach ($users as $i => $user) {
|
||||
if (!empty($user->hide)) {
|
||||
$users->forget($i);
|
||||
if ($exclude_hidden) {
|
||||
foreach ($users as $i => $user) {
|
||||
if (!empty($user->hide)) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,6 +511,8 @@ class Mailbox extends Model
|
||||
// Sort by full name
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
$users = \Eventy::filter('mailbox.users_assignable', $users, $this, $cache);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
@@ -474,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();
|
||||
@@ -487,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)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -561,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -615,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;
|
||||
@@ -625,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.
|
||||
*
|
||||
@@ -675,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -768,10 +912,88 @@ class Mailbox extends Model
|
||||
return $route;
|
||||
}
|
||||
|
||||
public function setMetaParam($param, $value)
|
||||
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 inOauthEnabled()
|
||||
{
|
||||
return $this->oauthEnabled()
|
||||
&& $this->in_username !== null && !strstr($this->in_username, '@');
|
||||
}
|
||||
|
||||
public function outOauthEnabled()
|
||||
{
|
||||
return $this->oauthEnabled()
|
||||
&& $this->out_username !== null && !strstr($this->out_username, '@')
|
||||
&& $this->out_server !== null && trim($this->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP;
|
||||
}
|
||||
|
||||
public function setEmailAttribute($value)
|
||||
{
|
||||
if ($value) {
|
||||
$this->attributes['email'] = Email::sanitizeEmail($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,17 @@ 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';
|
||||
const OAUTH_MICROSOFT_SMTP = 'smtp.office365.com';
|
||||
|
||||
/**
|
||||
* If reply is not extracted properly from the incoming email, add here a new separator.
|
||||
* Order is not important.
|
||||
@@ -56,7 +62,9 @@ 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"
|
||||
@@ -70,6 +78,7 @@ class Mail
|
||||
'<!-- originalMessage -->',
|
||||
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
|
||||
'--------------- Original Message ---------------',
|
||||
'-------- Αρχικό μήνυμα --------', // Greek
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -77,28 +86,67 @@ 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));
|
||||
// Configure mail driver according to Mailbox settings.
|
||||
$oauth = $mailbox->outOauthEnabled();
|
||||
|
||||
// SMTP
|
||||
// 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->out_username,
|
||||
'client_secret' => $mailbox->out_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_SENDING,
|
||||
\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER, [
|
||||
'error' => $error_message,
|
||||
'mailbox' => $mailbox->name,
|
||||
]);
|
||||
//throw new \Exception($error_message, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\Config::set('mail.driver', $mailbox->getMailDriverName());
|
||||
\Config::set('mail.from', $mailbox->getMailFrom($user_from, $conversation));
|
||||
|
||||
// SMTP.
|
||||
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
\Config::set('mail.host', $mailbox->out_server);
|
||||
\Config::set('mail.port', $mailbox->out_port);
|
||||
if (!$mailbox->out_username) {
|
||||
\Config::set('mail.username', null);
|
||||
\Config::set('mail.password', null);
|
||||
if ($oauth) {
|
||||
\Config::set('mail.auth_mode', 'XOAUTH2');
|
||||
\Config::set('mail.username', $mailbox->email);
|
||||
\Config::set('mail.password', $mailbox->oauthGetParam('a_token'));
|
||||
} else {
|
||||
\Config::set('mail.username', $mailbox->out_username);
|
||||
\Config::set('mail.password', $mailbox->out_password);
|
||||
if (!$mailbox->out_username) {
|
||||
\Config::set('mail.username', null);
|
||||
\Config::set('mail.password', null);
|
||||
} else {
|
||||
\Config::set('mail.username', $mailbox->out_username);
|
||||
\Config::set('mail.password', $mailbox->out_password);
|
||||
}
|
||||
}
|
||||
\Config::set('mail.encryption', $mailbox->getOutEncryptionName());
|
||||
}
|
||||
@@ -135,6 +183,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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,7 +209,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'));
|
||||
}
|
||||
@@ -170,7 +220,7 @@ class Mail
|
||||
/**
|
||||
* Replace mail vars in the text.
|
||||
*/
|
||||
public static function replaceMailVars($text, $data = [], $escape = false)
|
||||
public static function replaceMailVars($text, $data = [], $escape = false, $remove_non_replaced = false)
|
||||
{
|
||||
// Available variables to insert into email in UI.
|
||||
$vars = [];
|
||||
@@ -183,12 +233,18 @@ class Mail
|
||||
if (!empty($data['mailbox'])) {
|
||||
$vars['{%mailbox.email%}'] = $data['mailbox']->email;
|
||||
$vars['{%mailbox.name%}'] = $data['mailbox']->name;
|
||||
$vars['{%mailbox.fromName%}'] = $data['mailbox']->getMailFrom(!empty($data['user']) ? $data['user'] : null)['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();
|
||||
@@ -200,13 +256,28 @@ class Mail
|
||||
$vars['{%user.photoUrl%}'] = $data['user']->getPhotoUrl();
|
||||
}
|
||||
|
||||
$vars = \Eventy::filter('mail_vars.replace', $vars, $data);
|
||||
|
||||
if ($escape) {
|
||||
foreach ($vars as $i => $var) {
|
||||
$vars[$i] = htmlspecialchars($var);
|
||||
$vars[$i] = htmlspecialchars($var ?? '');
|
||||
$vars[$i] = nl2br($vars[$i]);
|
||||
}
|
||||
} else {
|
||||
foreach ($vars as $i => $var) {
|
||||
$vars[$i] = nl2br($var ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
return strtr($text, $vars);
|
||||
$result = strtr($text, $vars);
|
||||
|
||||
// Remove non-replaced placeholders.
|
||||
if ($remove_non_replaced) {
|
||||
$result = preg_replace('#\{%[^\.%\}]+\.[^%\}]+%\}#', '', $result ?? '');
|
||||
$result = trim($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +285,7 @@ class Mail
|
||||
*/
|
||||
public static function hasVars($text)
|
||||
{
|
||||
return preg_match('/({%|%})/', $text);
|
||||
return preg_match('/({%|%})/', $text ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,14 +317,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 = '';
|
||||
|
||||
@@ -266,6 +352,7 @@ class Mail
|
||||
} else {
|
||||
// System email
|
||||
\MailHelper::setSystemMailDriver();
|
||||
$smtp_logger = self::registerSmtpLogger();
|
||||
|
||||
$status_message = '';
|
||||
|
||||
@@ -281,15 +368,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,45 +386,65 @@ 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(),
|
||||
]);
|
||||
$result = [
|
||||
'result' => 'success',
|
||||
'msg' => '',
|
||||
'log' => '',
|
||||
];
|
||||
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
$client = null;
|
||||
|
||||
// Get folder
|
||||
$folder = $client->getFolder('INBOX');
|
||||
try {
|
||||
\Config::set('imap.options.debug', true);
|
||||
\Webklex\PHPIMAP\Connection\Protocols\ImapProtocol::$output_debug_log = false;
|
||||
|
||||
if (!$folder) {
|
||||
throw new \Exception('Could not get mailbox folder: INBOX', 1);
|
||||
}
|
||||
// Get unseen messages for a period
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
$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
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
|
||||
if (count($client->getErrors()) > 1) {
|
||||
$last_error = $client->getLastError();
|
||||
} else {
|
||||
$last_error = null;
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
|
||||
// Get folder
|
||||
$folder = $client->getFolder('INBOX');
|
||||
|
||||
if (!$folder) {
|
||||
$result['result'] = 'error';
|
||||
$result['msg'] = 'Could not get mailbox folder: INBOX';
|
||||
//throw new \Exception('Could not get mailbox folder: INBOX', 1);
|
||||
}
|
||||
// Get unseen messages for a period
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->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
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
|
||||
if (count($client->getErrors()) > 1) {
|
||||
$last_error = $client->getLastError();
|
||||
} else {
|
||||
$last_error = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($last_error) {
|
||||
//throw new \Exception($last_error, 1);
|
||||
$result['result'] = 'error';
|
||||
$result['msg'] = $last_error;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['result'] = 'error';
|
||||
$result['msg'] = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($last_error) {
|
||||
throw new \Exception($last_error, 1);
|
||||
} else {
|
||||
return true;
|
||||
if ($result['result'] == 'error') {
|
||||
$result['log'] = \Webklex\PHPIMAP\Connection\Protocols\ImapProtocol::getDebugLog();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +459,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) {
|
||||
@@ -444,7 +553,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]);
|
||||
@@ -453,6 +562,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
|
||||
@@ -465,11 +579,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) {
|
||||
@@ -561,15 +675,85 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -588,7 +772,7 @@ class Mail
|
||||
/**
|
||||
* Fetch IMAP message by Message-ID.
|
||||
*/
|
||||
public static function fetchMessage($mailbox, $message_id)
|
||||
public static function fetchMessage($mailbox, $message_id, $message_date = null)
|
||||
{
|
||||
$no_charset = false;
|
||||
|
||||
@@ -600,16 +784,32 @@ class Mail
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->connect();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID:');
|
||||
return null;
|
||||
}
|
||||
|
||||
$imap_folders = $mailbox->getInImapFolders();
|
||||
$imap_folders = \Eventy::filter('mail.fetch_message.imap_folders', $mailbox->getInImapFolders(), $mailbox);
|
||||
|
||||
foreach ($imap_folders as $folder_name) {
|
||||
try {
|
||||
$folder = $client->getFolder($folder_name);
|
||||
$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);
|
||||
$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);
|
||||
@@ -617,12 +817,20 @@ class Mail
|
||||
|
||||
$messages = $query->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
|
||||
$messages = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null)->get();
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -631,10 +839,323 @@ class Mail
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
\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,16 +2,15 @@
|
||||
|
||||
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';
|
||||
@@ -24,7 +23,30 @@ class WpApi
|
||||
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,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +57,13 @@ class WpApi
|
||||
{
|
||||
self::$lastError = null;
|
||||
|
||||
$options = ['connect_timeout' => 7]; // seconds
|
||||
|
||||
try {
|
||||
$response = Zttp::withOptions($options)->$method(self::url($endpoint, $alternative_api), $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(),
|
||||
@@ -52,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
|
||||
) {
|
||||
@@ -92,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.
|
||||
*/
|
||||
|
||||
349
app/Module.php
349
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 [];
|
||||
}
|
||||
@@ -231,6 +233,347 @@ class Module extends Model
|
||||
|
||||
public static function formatName($name)
|
||||
{
|
||||
return preg_replace("/ Module$/", '', $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();
|
||||
}
|
||||
}
|
||||
18
app/Observers/FollowerObserver.php
Normal file
18
app/Observers/FollowerObserver.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Follower;
|
||||
|
||||
class FollowerObserver
|
||||
{
|
||||
public function created(Follower $follower)
|
||||
{
|
||||
\Eventy::action('follower.created', $follower);
|
||||
}
|
||||
|
||||
public function deleted(Follower $follower)
|
||||
{
|
||||
\Eventy::action('follower.deleted', $follower);
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,30 @@ class ThreadObserver
|
||||
// Update data in conversation
|
||||
$conversation = $thread->conversation;
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
if (!$conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch date & time setting.
|
||||
$use_mail_date_on_fetching = config('app.use_mail_date_on_fetching');
|
||||
|
||||
if ($use_mail_date_on_fetching) {
|
||||
$now = $thread->created_at;
|
||||
}else{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
if (!in_array($thread->type, [Thread::TYPE_LINEITEM, Thread::TYPE_NOTE]) && $thread->state == Thread::STATE_PUBLISHED) {
|
||||
$conversation->threads_count++;
|
||||
}
|
||||
if (!in_array($thread->type, [Thread::TYPE_CUSTOMER])) {
|
||||
$conversation->user_updated_at = $now;
|
||||
}
|
||||
if (in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE]) && $thread->state == Thread::STATE_PUBLISHED) {
|
||||
|
||||
if ((in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE])
|
||||
|| ($conversation->isPhone() && in_array($thread->type, [Thread::TYPE_NOTE])))
|
||||
&& $thread->state == Thread::STATE_PUBLISHED
|
||||
) {
|
||||
// $conversation->cc = $thread->cc;
|
||||
// $conversation->bcc = $thread->bcc;
|
||||
$conversation->last_reply_at = $now;
|
||||
@@ -38,7 +54,9 @@ class ThreadObserver
|
||||
if (in_array($thread->type, [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
&& $thread->state == Thread::STATE_PUBLISHED
|
||||
&& !$thread->isForward()
|
||||
&& ($conversation->threads_count > 1 || $thread->type == Thread::TYPE_NOTE)
|
||||
// Otherwise preview is not set when conversation is created
|
||||
// outside of the web interface.
|
||||
//&& ($conversation->threads_count > 1 || $thread->type == Thread::TYPE_NOTE)
|
||||
) {
|
||||
$conversation->setPreview($thread->body);
|
||||
}
|
||||
@@ -58,7 +76,23 @@ class ThreadObserver
|
||||
// \Eventy::action('thread.created', $thread);
|
||||
// }
|
||||
|
||||
\App\Events\RealtimeConvNewThread::dispatchSelf($thread);
|
||||
\App\Events\RealtimeMailboxNewThread::dispatchSelf($conversation->mailbox_id);
|
||||
// Real time for user notifications is sent using events.
|
||||
if ($thread->type == Thread::TYPE_CUSTOMER
|
||||
|| ($thread->type == Thread::TYPE_MESSAGE && $thread->state == Thread::STATE_DRAFT)
|
||||
) {
|
||||
Conversation::refreshConversations($conversation, $thread);
|
||||
}
|
||||
|
||||
\Eventy::action('thread.created', $thread);
|
||||
}
|
||||
|
||||
public function deleting(Thread $thread)
|
||||
{
|
||||
\Eventy::action('thread.deleting', $thread);
|
||||
}
|
||||
|
||||
public function updated(Thread $thread)
|
||||
{
|
||||
\Eventy::action('thread.updated', $thread);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Follower;
|
||||
use App\Mailbox;
|
||||
use App\Subscription;
|
||||
use App\User;
|
||||
@@ -23,6 +24,15 @@ class UserObserver
|
||||
Subscription::addDefaultSubscriptions($user->id);
|
||||
}
|
||||
|
||||
public function creating(User $user)
|
||||
{
|
||||
// This is a hack for backward compatibility.
|
||||
if ($user->type == 0 && preg_match("#^fs.*@example\.org$#", $user->email)
|
||||
) {
|
||||
$user->type = User::TYPE_ROBOT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On user delete.
|
||||
*
|
||||
@@ -31,5 +41,6 @@ class UserObserver
|
||||
public function deleting(User $user)
|
||||
{
|
||||
$user->folders()->delete();
|
||||
Follower::whereIn('user_id', $user->id)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,14 +71,14 @@ class Option extends Model
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get($name, $default = false, $decode = true)
|
||||
public static function get($name, $default = false, $decode = true, $use_cache = true)
|
||||
{
|
||||
// If not passed, get default value from config
|
||||
if (func_num_args() == 1) {
|
||||
$default = self::getDefault($name, $default);
|
||||
}
|
||||
|
||||
if (isset(self::$cache[$name])) {
|
||||
if ($use_cache && isset(self::$cache[$name])) {
|
||||
if (self::$cache[$name] == self::CACHE_DEFAULT_VALUE) {
|
||||
return $default;
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,15 @@ class ConversationPolicy
|
||||
return true;
|
||||
} else {
|
||||
if ($conversation->mailbox->users->contains($user)) {
|
||||
return true;
|
||||
// Maybe user can see only assigned conversations.
|
||||
if (!\Eventy::filter('conversation.is_user_assignee', $conversation->user_id == $user->id, $conversation, $user->id)
|
||||
&& $conversation->created_by_user_id != $user->id
|
||||
&& $user->canSeeOnlyAssignedConversations()
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -45,7 +53,15 @@ class ConversationPolicy
|
||||
return true;
|
||||
} else {
|
||||
if ($conversation->mailbox->users_cached->contains($user)) {
|
||||
return true;
|
||||
// Maybe user can see only assigned conversations.
|
||||
if (!\Eventy::filter('conversation.is_user_assignee', $conversation->user_id == $user->id, $conversation, $user->id)
|
||||
&& $conversation->created_by_user_id != $user->id
|
||||
&& $user->canSeeOnlyAssignedConversations()
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class MailboxPolicy
|
||||
*/
|
||||
public function updateAutoReply(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'auto')) {
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, Mailbox::ACCESS_PERM_AUTO_REPLIES)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -124,7 +124,7 @@ class MailboxPolicy
|
||||
*/
|
||||
public function updatePermissions(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'perm')) {
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, Mailbox::ACCESS_PERM_PERMISSIONS)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -141,7 +141,7 @@ class MailboxPolicy
|
||||
*/
|
||||
public function updateSettings(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'edit')) {
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, Mailbox::ACCESS_PERM_EDIT)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -158,7 +158,7 @@ class MailboxPolicy
|
||||
*/
|
||||
public function updateEmailSignature(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'sig')) {
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, Mailbox::ACCESS_PERM_SIGNATURE)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -30,4 +30,13 @@ class ThreadPolicy
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(User $user, Thread $thread)
|
||||
{
|
||||
if ($thread->created_by_user_id == $user->id) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,15 +105,17 @@ class UserPolicy
|
||||
*/
|
||||
public function viewMailboxMenu(User $user)
|
||||
{
|
||||
if ($user->isAdmin() || \Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
|
||||
return true;
|
||||
// hasManageMailboxAccess creates an extra query on each page,
|
||||
// to avoid this we don't show Manage menu to users,
|
||||
// user can manage mailboxes from dashboard.
|
||||
} else if ($user->hasManageMailboxAccess()) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
// if ($user->isAdmin() || \Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
|
||||
// return true;
|
||||
// // hasManageMailboxAccess creates an extra query on each page,
|
||||
// // to avoid this we don't show Manage menu to users,
|
||||
// // user can manage mailboxes from dashboard.
|
||||
// } else if ($user->hasManageMailboxAccess()) {
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
\App\Email::observe(\App\Observers\EmailObserver::class);
|
||||
\App\User::observe(\App\Observers\UserObserver::class);
|
||||
\App\Conversation::observe(\App\Observers\ConversationObserver::class);
|
||||
\App\Customer::observe(\App\Observers\CustomerObserver::class);
|
||||
\App\Thread::observe(\App\Observers\ThreadObserver::class);
|
||||
\App\Attachment::observe(\App\Observers\AttachmentObserver::class);
|
||||
\App\Follower::observe(\App\Observers\FollowerObserver::class);
|
||||
\Illuminate\Notifications\DatabaseNotification::observe(\App\Observers\DatabaseNotificationObserver::class);
|
||||
}
|
||||
|
||||
@@ -38,7 +41,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
// Forse HTTPS if using CloudFlare "Flexible SSL"
|
||||
// https://support.cloudflare.com/hc/en-us/articles/200170416-What-do-the-SSL-options-mean-
|
||||
if (\Config::get('app.force_https') == 'true') {
|
||||
if (\Helper::isHttps()) {
|
||||
// $_SERVER['HTTPS'] = 'on';
|
||||
// $_SERVER['SERVER_PORT'] = '443';
|
||||
$this->app['url']->forceScheme('https');
|
||||
|
||||
@@ -17,6 +17,10 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Listeners\ProcessSwiftMessage',
|
||||
],
|
||||
|
||||
'Illuminate\Mail\Events\MessageSent' => [
|
||||
'App\Listeners\RestartSwiftMailer',
|
||||
],
|
||||
|
||||
'Illuminate\Auth\Events\Registered' => [
|
||||
'App\Listeners\LogRegisteredUser',
|
||||
],
|
||||
@@ -68,6 +72,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\UserReplied' => [
|
||||
'App\Listeners\SendReplyToCustomer',
|
||||
'App\Listeners\SendNotificationToUsers',
|
||||
'App\Listeners\RefreshConversations',
|
||||
],
|
||||
|
||||
'App\Events\CustomerReplied' => [
|
||||
@@ -77,6 +82,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\UserCreatedConversation' => [
|
||||
'App\Listeners\SendReplyToCustomer',
|
||||
'App\Listeners\SendNotificationToUsers',
|
||||
'App\Listeners\RefreshConversations',
|
||||
],
|
||||
|
||||
'App\Events\CustomerCreatedConversation' => [
|
||||
@@ -86,6 +92,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
|
||||
'App\Events\UserAddedNote' => [
|
||||
'App\Listeners\SendNotificationToUsers',
|
||||
'App\Listeners\RefreshConversations',
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class SendLog extends Model
|
||||
* https://documentation.mailgun.com/en/latest/api-events.html#event-types.
|
||||
*/
|
||||
const STATUS_ACCEPTED = 1; // accepted (for delivery)
|
||||
const STATUS_SEND_INTERMEDIATE_ERROR = 10;
|
||||
const STATUS_SEND_ERROR = 2;
|
||||
const STATUS_DELIVERY_SUCCESS = 4;
|
||||
const STATUS_DELIVERY_ERROR = 5; // rejected, failed
|
||||
@@ -42,6 +43,7 @@ class SendLog extends Model
|
||||
* @var [type]
|
||||
*/
|
||||
public static $status_errors = [
|
||||
self::STATUS_SEND_INTERMEDIATE_ERROR,
|
||||
self::STATUS_SEND_ERROR,
|
||||
self::STATUS_DELIVERY_ERROR,
|
||||
];
|
||||
@@ -92,8 +94,11 @@ class SendLog extends Model
|
||||
/**
|
||||
* Save log record.
|
||||
*/
|
||||
public static function log($thread_id, $message_id, $email, $mail_type, $status, $customer_id = null, $user_id = null, $status_message = null)
|
||||
public static function log($thread_id, $message_id, $email, $mail_type, $status, $customer_id = null, $user_id = null, $status_message = null, $smtp_queue_id = null)
|
||||
{
|
||||
// Sanitize status message - remove SMTP username and password.
|
||||
$status_message = \MailHelper::sanitizeSmtpStatusMessage($status_message);
|
||||
|
||||
$send_log = new self();
|
||||
$send_log->thread_id = $thread_id;
|
||||
$send_log->message_id = $message_id;
|
||||
@@ -103,7 +108,15 @@ class SendLog extends Model
|
||||
$send_log->customer_id = $customer_id;
|
||||
$send_log->user_id = $user_id;
|
||||
$send_log->status_message = $status_message;
|
||||
$send_log->save();
|
||||
if ($smtp_queue_id) {
|
||||
$send_log->smtp_queue_id = $smtp_queue_id;
|
||||
}
|
||||
try {
|
||||
$send_log->save();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, 'Error occurred saving a record to `send_logs` table. ');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Mailbox;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use App\Notifications\WebsiteNotification;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -26,8 +27,8 @@ class Subscription extends Model
|
||||
const EVENT_CONVERSATION_ASSIGNED_TO_ME = 2;
|
||||
const EVENT_CONVERSATION_ASSIGNED = 6;
|
||||
const EVENT_FOLLOWED_CONVERSATION_UPDATED = 13;
|
||||
const EVENT_I_AM_MENTIONED = 14;
|
||||
const EVENT_MY_TEAM_MENTIONED = 15;
|
||||
|
||||
//const EVENT_MY_TEAM_MENTIONED = 15;
|
||||
// Notify me when a customer replies…
|
||||
const EVENT_CUSTOMER_REPLIED_TO_UNASSIGNED = 4;
|
||||
const EVENT_CUSTOMER_REPLIED_TO_MY = 3;
|
||||
@@ -41,6 +42,7 @@ class Subscription extends Model
|
||||
const MEDIUM_EMAIL = 1; // This is also website notifications
|
||||
const MEDIUM_BROWSER = 2; // Browser push notification
|
||||
const MEDIUM_MOBILE = 3;
|
||||
const MEDIUM_MENU = 10; // Notifications menu
|
||||
|
||||
public static $mediums = [
|
||||
self::MEDIUM_EMAIL,
|
||||
@@ -52,25 +54,23 @@ class Subscription extends Model
|
||||
self::MEDIUM_EMAIL => [
|
||||
self::EVENT_CONVERSATION_ASSIGNED_TO_ME,
|
||||
self::EVENT_FOLLOWED_CONVERSATION_UPDATED,
|
||||
self::EVENT_I_AM_MENTIONED,
|
||||
self::EVENT_MY_TEAM_MENTIONED,
|
||||
//self::EVENT_MY_TEAM_MENTIONED,
|
||||
self::EVENT_CUSTOMER_REPLIED_TO_MY,
|
||||
self::EVENT_USER_REPLIED_TO_MY,
|
||||
],
|
||||
self::MEDIUM_BROWSER => [
|
||||
self::EVENT_CONVERSATION_ASSIGNED_TO_ME,
|
||||
self::EVENT_FOLLOWED_CONVERSATION_UPDATED,
|
||||
self::EVENT_I_AM_MENTIONED,
|
||||
self::EVENT_MY_TEAM_MENTIONED,
|
||||
//self::EVENT_MY_TEAM_MENTIONED,
|
||||
self::EVENT_CUSTOMER_REPLIED_TO_MY,
|
||||
self::EVENT_USER_REPLIED_TO_MY,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* List of events that occured.
|
||||
* List of events that occurred.
|
||||
*/
|
||||
public static $occured_events = [];
|
||||
public static $occurred_events = [];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
@@ -96,7 +96,12 @@ class Subscription extends Model
|
||||
*/
|
||||
public static function addDefaultSubscriptions($user_id)
|
||||
{
|
||||
self::saveFromArray(self::$default_subscriptions, $user_id);
|
||||
self::saveFromArray(self::getDefaultSubscriptions(), $user_id);
|
||||
}
|
||||
|
||||
public static function getDefaultSubscriptions()
|
||||
{
|
||||
return Option::get('subscription_defaults', self::$default_subscriptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +159,18 @@ class Subscription extends Model
|
||||
*/
|
||||
public static function usersToNotify($event_type, $conversation, $threads, $mailbox_user_ids = null)
|
||||
{
|
||||
$thread = $threads[0];
|
||||
$users_to_notify = [];
|
||||
$thread = null;
|
||||
|
||||
if (isset($threads[0])) {
|
||||
$thread = $threads[0];
|
||||
} elseif (count($threads)) {
|
||||
$thread = array_shift(array_values($threads));
|
||||
}
|
||||
|
||||
if (!$thread) {
|
||||
return $users_to_notify;
|
||||
}
|
||||
|
||||
// Ignore imported threads.
|
||||
if ($thread->imported) {
|
||||
@@ -199,8 +215,10 @@ class Subscription extends Model
|
||||
$events[] = self::EVENT_USER_REPLIED_TO_UNASSIGNED;
|
||||
}
|
||||
break;
|
||||
// todo: EVENT_I_AM_MENTIONED, EVENT_MY_TEAM_MENTIONED
|
||||
}
|
||||
|
||||
$events = \Eventy::filter('subscription.events_by_type', $events, $event_type, $thread);
|
||||
|
||||
// Check if assigned user changed
|
||||
$user_changed = false;
|
||||
if ($event_type != self::EVENT_TYPE_ASSIGNED && $event_type != self::EVENT_TYPE_NEW) {
|
||||
@@ -233,11 +251,13 @@ class Subscription extends Model
|
||||
->whereIn('event', $events)
|
||||
->get();
|
||||
|
||||
$subscriptions = \Eventy::filter('subscription.subscriptions', $subscriptions, $conversation, $events, $thread);
|
||||
|
||||
// Filter subscribers
|
||||
$users_to_notify = [];
|
||||
foreach ($subscriptions as $i => $subscription) {
|
||||
// Actions on conversation where user is assignee
|
||||
if (in_array($subscription->event, [self::EVENT_CONVERSATION_ASSIGNED_TO_ME, self::EVENT_CUSTOMER_REPLIED_TO_MY, self::EVENT_USER_REPLIED_TO_MY]) && $conversation->user_id != $subscription->user_id
|
||||
if (in_array($subscription->event, [self::EVENT_CONVERSATION_ASSIGNED_TO_ME, self::EVENT_CUSTOMER_REPLIED_TO_MY, self::EVENT_USER_REPLIED_TO_MY])
|
||||
&& ($conversation->user_id != $subscription->user_id && !\Eventy::filter('subscription.is_user_assignee', false, $subscription, $conversation))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -250,28 +270,43 @@ class Subscription extends Model
|
||||
}
|
||||
|
||||
// Skip if user muted notifications for this mailbox
|
||||
if ($subscription->user->isAdmin()) {
|
||||
//if ($subscription->user->isAdmin()) {
|
||||
|
||||
// Mute notifications for events not related directly to the user.
|
||||
if (!in_array($subscription->event, [self::EVENT_CONVERSATION_ASSIGNED_TO_ME, self::EVENT_FOLLOWED_CONVERSATION_UPDATED, self::EVENT_I_AM_MENTIONED, self::EVENT_CUSTOMER_REPLIED_TO_MY, self::EVENT_USER_REPLIED_TO_MY])
|
||||
) {
|
||||
$mailbox_settings = $conversation->mailbox->getUserSettings($subscription->user_id);
|
||||
// Mute notifications for events not related directly to the user.
|
||||
if (!in_array($subscription->event, [self::EVENT_CONVERSATION_ASSIGNED_TO_ME, self::EVENT_FOLLOWED_CONVERSATION_UPDATED, self::EVENT_CUSTOMER_REPLIED_TO_MY, self::EVENT_USER_REPLIED_TO_MY])
|
||||
&& !\Eventy::filter('subscription.is_related_to_user', false, $subscription, $thread)
|
||||
) {
|
||||
$mailbox_settings = $conversation->mailbox->getUserSettings($subscription->user_id);
|
||||
|
||||
if (!empty($mailbox_settings->mute)) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($mailbox_settings->mute)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
//}
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3843#issuecomment-1974811457
|
||||
if ($conversation->user_id != $subscription->user_id
|
||||
&& $subscription->user->canSeeOnlyAssignedConversations()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\Eventy::filter('subscription.filter_out', false, $subscription, $thread)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$users_to_notify[$subscription->medium][] = $subscription->user;
|
||||
$users_to_notify[$subscription->medium] = array_unique($users_to_notify[$subscription->medium]);
|
||||
}
|
||||
|
||||
// Add menu notifications, for example.
|
||||
$users_to_notify = \Eventy::filter('subscription.users_to_notify', $users_to_notify, $event_type, $events, $thread);
|
||||
|
||||
return $users_to_notify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process events which occured.
|
||||
* Process events which occurred.
|
||||
*/
|
||||
public static function processEvents()
|
||||
{
|
||||
@@ -280,7 +315,7 @@ class Subscription extends Model
|
||||
$delay = now()->addSeconds(Conversation::UNDO_TIMOUT);
|
||||
|
||||
// Collect into notify array information about all users who need to be notified
|
||||
foreach (self::$occured_events as $event) {
|
||||
foreach (self::$occurred_events as $event) {
|
||||
// Get mailbox users ids
|
||||
$mailbox_user_ids = [];
|
||||
foreach (self::$mediums as $medium) {
|
||||
@@ -302,14 +337,14 @@ class Subscription extends Model
|
||||
$threads = $event['conversation']->getThreads();
|
||||
break;
|
||||
} else {
|
||||
$users = $notify[$medium][$event['conversation']->id]['users'];
|
||||
$users[$medium] = $notify[$medium][$event['conversation']->id]['users'];
|
||||
$threads = $notify[$medium][$event['conversation']->id]['threads'];
|
||||
}
|
||||
}
|
||||
|
||||
$users_to_notify = self::usersToNotify($event['event_type'], $event['conversation'], $threads, $mailbox_user_ids);
|
||||
|
||||
if (!$users_to_notify) {
|
||||
if (!$users_to_notify || !is_array($users_to_notify)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -324,8 +359,9 @@ class Subscription extends Model
|
||||
|
||||
if (count($medium_users_to_notify)) {
|
||||
$notify[$medium][$event['conversation']->id] = [
|
||||
// Users subarray contains all users who need to receive notification for all events
|
||||
'users' => array_unique(array_merge($users, $medium_users_to_notify)),
|
||||
// Users subarray contains all users who need to receive notification
|
||||
// for all events for the media.
|
||||
'users' => array_unique(array_merge($users[$medium] ?? [], $medium_users_to_notify)),
|
||||
'conversation' => $event['conversation'],
|
||||
'threads' => $threads,
|
||||
'mailbox_user_ids' => $mailbox_user_ids,
|
||||
@@ -334,17 +370,27 @@ class Subscription extends Model
|
||||
}
|
||||
}
|
||||
|
||||
// - Notify by email
|
||||
// - Real-time menu notification (uses same medium as for email)
|
||||
// - Email notification (better to create them first)
|
||||
if (!empty($notify[self::MEDIUM_EMAIL])) {
|
||||
// Email notification (better to create them first)
|
||||
foreach ($notify[self::MEDIUM_EMAIL] as $conversation_id => $notify_info) {
|
||||
\App\Jobs\SendNotificationToUsers::dispatch($notify_info['users'], $notify_info['conversation'], $notify_info['threads'])
|
||||
->delay($delay)
|
||||
->onQueue('emails');
|
||||
}
|
||||
// - Menu notification (uses same medium as for email)
|
||||
foreach ($notify[self::MEDIUM_EMAIL] as $notify_info) {
|
||||
}
|
||||
|
||||
// - Menu notification (uses same medium as for Email, if email notifications are disabled - use Browser notificaitons)
|
||||
if (!empty($notify[self::MEDIUM_EMAIL])
|
||||
|| !empty($notify[self::MEDIUM_BROWSER])
|
||||
|| !empty($notify[self::MEDIUM_MENU])
|
||||
) {
|
||||
if (!empty($notify[self::MEDIUM_EMAIL])) {
|
||||
$notify_menu = $notify[self::MEDIUM_EMAIL] ?? [];
|
||||
} else {
|
||||
$notify_menu = $notify[self::MEDIUM_BROWSER] ?? [];
|
||||
}
|
||||
$notify_menu = $notify_menu + ($notify[self::MEDIUM_MENU] ?? []);
|
||||
foreach ($notify_menu as $notify_info) {
|
||||
$website_notification = new WebsiteNotification($notify_info['conversation'], self::chooseThread($notify_info['threads']));
|
||||
$website_notification->delay($delay);
|
||||
\Notification::send($notify_info['users'], $website_notification);
|
||||
@@ -362,11 +408,12 @@ class Subscription extends Model
|
||||
$thread_id = self::chooseThread($notify_info['threads'])->id;
|
||||
|
||||
foreach ($notify_info['users'] as $user) {
|
||||
$broadcast_id = $thread_id.'_'.$user->id;
|
||||
$mediums = [$medium];
|
||||
if (!empty($broadcasts[$thread_id]['mediums'])) {
|
||||
$mediums = array_unique(array_merge($mediums, $broadcasts[$thread_id]['mediums']));
|
||||
if (!empty($broadcasts[$broadcast_id]['mediums'])) {
|
||||
$mediums = array_unique(array_merge($mediums, $broadcasts[$broadcast_id]['mediums']));
|
||||
}
|
||||
$broadcasts[$thread_id] = [
|
||||
$broadcasts[$broadcast_id] = [
|
||||
'user' => $user,
|
||||
'conversation' => $notify_info['conversation'],
|
||||
'threads' => $notify_info['threads'],
|
||||
@@ -376,7 +423,7 @@ class Subscription extends Model
|
||||
}
|
||||
}
|
||||
// \Notification::sendNow($notify_info['users'], new BroadcastNotification($notify_info['conversation'], $notify_info['threads'][0]));
|
||||
foreach ($broadcasts as $thread_id => $to_broadcast) {
|
||||
foreach ($broadcasts as $broadcast_id => $to_broadcast) {
|
||||
$broadcast_notification = new BroadcastNotification($to_broadcast['conversation'], self::chooseThread($to_broadcast['threads']), $to_broadcast['mediums']);
|
||||
$broadcast_notification->delay($delay);
|
||||
$to_broadcast['user']->notify($broadcast_notification);
|
||||
@@ -385,7 +432,7 @@ class Subscription extends Model
|
||||
// - Mobile
|
||||
\Eventy::action('subscription.process_events', $notify);
|
||||
|
||||
self::$occured_events = [];
|
||||
self::$occurred_events = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,7 +459,7 @@ class Subscription extends Model
|
||||
*/
|
||||
public static function registerEvent($event_type, $conversation, $caused_by_user_id, $process_now = false)
|
||||
{
|
||||
self::$occured_events[] = [
|
||||
self::$occurred_events[] = [
|
||||
'event_type' => $event_type,
|
||||
'conversation' => $conversation,
|
||||
'caused_by_user_id' => $caused_by_user_id,
|
||||
@@ -420,7 +467,7 @@ class Subscription extends Model
|
||||
|
||||
// Automatically add EVENT_TYPE_UPDATED
|
||||
if (!in_array($event_type, [self::EVENT_TYPE_UPDATED, self::EVENT_TYPE_NEW])) {
|
||||
self::$occured_events[] = [
|
||||
self::$occurred_events[] = [
|
||||
'event_type' => self::EVENT_TYPE_UPDATED,
|
||||
'conversation' => $conversation,
|
||||
'caused_by_user_id' => $caused_by_user_id,
|
||||
|
||||
384
app/Thread.php
384
app/Thread.php
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Mailbox;
|
||||
use App\SendLog;
|
||||
use App\Events\ConversationStatusChanged;
|
||||
use App\Events\ConversationUserChanged;
|
||||
use App\Events\UserAddedNote;
|
||||
@@ -56,6 +58,7 @@ class Thread extends Model
|
||||
//self::TYPE_FORWARDPARENT => 'forwardparent',
|
||||
// forwardchild is the type set on the first thread of the new forwarded conversation.
|
||||
//self::TYPE_FORWARDCHILD => 'forwardchild',
|
||||
// Not used.
|
||||
self::TYPE_CHAT => 'chat',
|
||||
];
|
||||
|
||||
@@ -158,6 +161,20 @@ class Thread extends Model
|
||||
const META_PREV_CONVERSATION = 'pc';
|
||||
const META_MERGED_WITH_CONV = 'mwc';
|
||||
const META_MERGED_INTO_CONV = 'mic';
|
||||
const META_FORWARD_PARENT_CONVERSATION_NUMBER = 'fw_pcn';
|
||||
const META_FORWARD_PARENT_CONVERSATION_ID = 'fw_pci';
|
||||
const META_FORWARD_PARENT_THREAD_ID = 'fw_pti';
|
||||
const META_FORWARD_CHILD_CONVERSATION_NUMBER = 'fw_ccn';
|
||||
const META_FORWARD_CHILD_CONVERSATION_ID = 'fw_cci';
|
||||
|
||||
// At some stage metas have been renamed.
|
||||
public static $meta_fw_backward_compat = [
|
||||
self::META_FORWARD_PARENT_CONVERSATION_NUMBER => 'forward_parent_conversation_number',
|
||||
self::META_FORWARD_PARENT_CONVERSATION_ID => 'forward_parent_conversation_id',
|
||||
self::META_FORWARD_PARENT_THREAD_ID => 'forward_parent_thread_id',
|
||||
self::META_FORWARD_CHILD_CONVERSATION_NUMBER => 'forward_child_conversation_number',
|
||||
self::META_FORWARD_CHILD_CONVERSATION_ID => 'forward_child_conversation_id',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'opened_at',
|
||||
@@ -287,15 +304,35 @@ class Thread extends Model
|
||||
$body = $this->body;
|
||||
}
|
||||
|
||||
if ($body === null) {
|
||||
$body = '';
|
||||
}
|
||||
|
||||
// Change "background:" to "background-color:".
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2560
|
||||
// Keep in mind that with large texts preg_replace() may return null.
|
||||
$body = preg_replace("/(<[^<>]+style=[\"'][^\"']*)background: *([^;() ]+[;\"'])/", '$1background-color:$2', $body) ?: $body;
|
||||
|
||||
// Cut out "collapse" class as it hides elements.
|
||||
$body = preg_replace("/(<[^<>\r\n]+class=([\"'][^\"']* |[\"']))(collapse|hidden)([\"' ])/", '$1$4', $body) ?: $body;
|
||||
|
||||
// Remove only the <!--[if !mso]><!--> and <!--<![endif]--> around the elements.
|
||||
// https://github.com/freescout-helpdesk/freescout/pull/3865#issuecomment-1990758149
|
||||
$body = preg_replace('/<!\-\-\[if [^>]+\]><!\-\->(.*?)<![ ]+\-\-<!\[endif\]\-\->/s', '$1', $body);
|
||||
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3894
|
||||
// Remove <!--[if !mso]><!--> and <!--<![endif]--> comments, preserving the data inside
|
||||
//$body = preg_replace('/(<!\-\-\[if [^>]+\]>|<!\[endif\]\-\->)/', '', $body);
|
||||
|
||||
return \Helper::purifyHtml($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert body to plain text.
|
||||
*/
|
||||
public function getBodyAsText()
|
||||
public function getBodyAsText($options = ['width' => 0])
|
||||
{
|
||||
return \Helper::htmlToText($this->body, true);
|
||||
return \Helper::htmlToText($this->body, true, $options);
|
||||
}
|
||||
|
||||
public function getBodyWithFormatedLinks(string $body = '') :string
|
||||
@@ -304,7 +341,31 @@ class Thread extends Model
|
||||
$body = $this->body;
|
||||
}
|
||||
|
||||
return \Helper::linkify($this->getCleanBody($body));
|
||||
$body = \Helper::linkify($this->getCleanBody($body));
|
||||
|
||||
// Add target="_blank" to links.
|
||||
$pattern = '/<a(.*?)?href=[\'"]?[\'"]?(.*?)?>/i';
|
||||
|
||||
$body = preg_replace_callback($pattern, function($m){
|
||||
$tpl = array_shift($m);
|
||||
$href = isset($m[1]) ? $m[1] : null;
|
||||
|
||||
if (preg_match('/target=[\'"]?(.*?)[\'"]?/i', $tpl)) {
|
||||
return $tpl;
|
||||
}
|
||||
|
||||
if (trim($href) && 0 === strpos($href, '#')) {
|
||||
// Anchor links.
|
||||
return $tpl;
|
||||
}
|
||||
|
||||
return preg_replace_callback('/href=/i', function($m2){
|
||||
return sprintf('target="_blank" %s', array_shift($m2));
|
||||
}, $tpl);
|
||||
|
||||
}, $body) ?: $body;
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -512,7 +573,12 @@ class Thread extends Model
|
||||
return \App\User::getDeletedUser();
|
||||
}
|
||||
} else {
|
||||
return $this->created_by_customer;
|
||||
// In some cases the created_by_customer can be empty.
|
||||
if ($this->created_by_customer) {
|
||||
return $this->created_by_customer;
|
||||
} else {
|
||||
return \App\Customer::getDummyCustomer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +610,9 @@ class Thread extends Model
|
||||
$person = '';
|
||||
|
||||
if ($this->type == self::TYPE_CUSTOMER) {
|
||||
$person = $this->customer_cached->getFullName(true);
|
||||
if ($this->customer_cached) {
|
||||
$person = $this->customer_cached->getFullName(true);
|
||||
}
|
||||
} elseif ($this->state == self::STATE_DRAFT && !empty($this->edited_by_user_id)) {
|
||||
// Draft
|
||||
if (auth()->user() && $this->edited_by_user_id == auth()->user()->id) {
|
||||
@@ -568,9 +636,10 @@ class Thread extends Model
|
||||
|
||||
/**
|
||||
* Get action text.
|
||||
* $by_user - user who performed the action.
|
||||
* $person must be already escaped.
|
||||
*/
|
||||
public function getActionText($conversation_number = '', $escape = false, $strip_tags = false, $by_user = null, $person = '')
|
||||
public function getActionText($conversation_number = '', $escape = false, $strip_tags = false, $by_user = null, $person = '', $viewed_by_user = null)
|
||||
{
|
||||
$did_this = '';
|
||||
|
||||
@@ -589,7 +658,7 @@ class Thread extends Model
|
||||
$assignee = htmlspecialchars($assignee);
|
||||
}
|
||||
if ($conversation_number) {
|
||||
$did_this = __(':person assigned :assignee convsersation #:conversation_number', ['assignee' => $assignee, 'conversation_number' => $conversation_number]);
|
||||
$did_this = __(':person assigned :assignee conversation #:conversation_number', ['assignee' => $assignee, 'conversation_number' => $conversation_number]);
|
||||
} else {
|
||||
$did_this = __(":person assigned to :assignee", ['assignee' => $assignee]);
|
||||
}
|
||||
@@ -597,11 +666,14 @@ class Thread extends Model
|
||||
if ($conversation_number) {
|
||||
$did_this = __(':person changed the customer to :customer in conversation #:conversation_number', ['customer' => $this->customer->getFullName(true), 'conversation_number' => $conversation_number]);
|
||||
} else {
|
||||
$customer_name = $this->customer_cached->getFullName(true);
|
||||
$customer_name = '';
|
||||
if ($this->customer_cached) {
|
||||
$customer_name = $this->customer_cached->getFullName(true);
|
||||
}
|
||||
if ($escape) {
|
||||
$customer_name = htmlspecialchars($customer_name);
|
||||
}
|
||||
$did_this = __(":person changed the customer to :customer", ['customer' => '<a href="'.$this->customer_cached->url().'" title="'.$this->action_data.'" class="link-black">'.$customer_name.'</a>']);
|
||||
$did_this = __(":person changed the customer to :customer", ['customer' => '<a href="'.($this->customer_cached ? $this->customer_cached->url() : '').'" title="'.$this->action_data.'" class="link-black">'.$customer_name.'</a>']);
|
||||
}
|
||||
} elseif ($this->action_type == self::ACTION_TYPE_DELETED_TICKET) {
|
||||
$did_this = __(":person deleted");
|
||||
@@ -618,7 +690,11 @@ class Thread extends Model
|
||||
if ($merge_conversation) {
|
||||
$merge_conversation_number = $merge_conversation->number;
|
||||
}
|
||||
$did_this = __(":person merged into conversation #:conversation_number", ['conversation_number' => $merge_conversation_number]);
|
||||
if ($merge_conversation) {
|
||||
$did_this = __(":person merged into conversation #:conversation_number", ['conversation_number' => '<a href="'.$merge_conversation->url().'" class="link-black">'.$merge_conversation_number.'</a>']);
|
||||
} else {
|
||||
$did_this = __(":person merged into conversation #:conversation_number", ['conversation_number' => $merge_conversation_number]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($this->state == self::STATE_DRAFT) {
|
||||
@@ -629,7 +705,7 @@ class Thread extends Model
|
||||
}
|
||||
} else {
|
||||
if ($this->isForwarded()) {
|
||||
$did_this = __(':person forwarded a conversation #:forward_parent_conversation_number', ['forward_parent_conversation_number' => $this->getMeta('forward_parent_conversation_number')]);
|
||||
$did_this = __(':person forwarded a conversation #:forward_parent_conversation_number', ['forward_parent_conversation_number' => $this->getMetaFw(self::META_FORWARD_PARENT_CONVERSATION_NUMBER)]);
|
||||
} elseif ($this->first) {
|
||||
$did_this = __(':person started a new conversation #:conversation_number', ['conversation_number' => $conversation_number]);
|
||||
} elseif ($this->type == self::TYPE_NOTE) {
|
||||
@@ -639,7 +715,7 @@ class Thread extends Model
|
||||
}
|
||||
}
|
||||
|
||||
$did_this = \Eventy::filter('thread.action_text', $did_this, $this, $conversation_number, $escape);
|
||||
$did_this = \Eventy::filter('thread.action_text', $did_this, $this, $conversation_number, $escape, $viewed_by_user);
|
||||
|
||||
if ($strip_tags) {
|
||||
$did_this = strip_tags($did_this);
|
||||
@@ -659,11 +735,11 @@ class Thread extends Model
|
||||
/**
|
||||
* Description of what happened.
|
||||
*/
|
||||
public function getActionDescription($conversation_number, $escape = true)
|
||||
public function getActionDescription($conversation_number, $escape = true, $viewed_by_user = null)
|
||||
{
|
||||
// Person
|
||||
$person = $this->getActionPerson($conversation_number);
|
||||
$did_this = $this->getActionText($conversation_number);
|
||||
$did_this = $this->getActionText($conversation_number, false, false, null, '', $viewed_by_user);
|
||||
|
||||
if ($escape) {
|
||||
$person = htmlspecialchars($person);
|
||||
@@ -688,6 +764,10 @@ class Thread extends Model
|
||||
{
|
||||
$this->deteleAttachments();
|
||||
$this->delete();
|
||||
|
||||
if ($this->isNote()) {
|
||||
Conversation::updatePreview($this->conversation_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -726,7 +806,11 @@ class Thread extends Model
|
||||
{
|
||||
// Created by customer
|
||||
if ($this->source_via == self::PERSON_CUSTOMER) {
|
||||
return $this->getCreatedBy()->getFirstName(true);
|
||||
if ($this->getCreatedBy()) {
|
||||
return $this->getCreatedBy()->getFirstName(true);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Created by user
|
||||
@@ -737,7 +821,14 @@ class Thread extends Model
|
||||
$name = $mailbox->name;
|
||||
|
||||
if ($mailbox->from_name == Mailbox::FROM_NAME_CUSTOM && $mailbox->from_name_custom) {
|
||||
$name = $mailbox->from_name_custom;
|
||||
$data = [
|
||||
'mailbox' => $mailbox,
|
||||
'mailbox_from_name' => '', // To avoid recursion.
|
||||
'conversation' => $this->conversation,
|
||||
// If we reach here it means the thread has been created by user.
|
||||
'user' => $this->getCreatedBy(),
|
||||
];
|
||||
$name = \MailHelper::replaceMailVars($mailbox->from_name_custom, $data, false, true);
|
||||
} elseif ($mailbox->from_name == Mailbox::FROM_NAME_USER && $this->getCreatedBy()) {
|
||||
$name = $this->getCreatedBy()->getFirstName(true);
|
||||
}
|
||||
@@ -803,6 +894,9 @@ class Thread extends Model
|
||||
} else {
|
||||
$send_status_data = $new_data;
|
||||
}
|
||||
if (!empty($send_status_data['msg'])) {
|
||||
$send_status_data['msg'] = \MailHelper::sanitizeSmtpStatusMessage($send_status_data['msg']);
|
||||
}
|
||||
$this->send_status_data = \Helper::jsonEncodeUtf8($send_status_data);
|
||||
} else {
|
||||
$this->send_status_data = null;
|
||||
@@ -900,7 +994,9 @@ class Thread extends Model
|
||||
$customer = $conversation->customer;
|
||||
}
|
||||
|
||||
$user_id = $data['user_id'] ?? null;
|
||||
// User which creatd the thread should be passed in created_by_user_id.
|
||||
// user_id is check for backward compatibility.
|
||||
$user_id = $data['created_by_user_id'] ?? $data['user_id'] ?? null;
|
||||
|
||||
// Check type.
|
||||
if ($data['type'] == Thread::TYPE_CUSTOMER && empty($customer)) {
|
||||
@@ -951,6 +1047,11 @@ class Thread extends Model
|
||||
$thread->first = true;
|
||||
}
|
||||
|
||||
// Assignee.
|
||||
if (empty($data['user_id'])) {
|
||||
$thread->user_id = $conversation->user_id;
|
||||
}
|
||||
|
||||
// Process attachments.
|
||||
if (!empty($data['attachments'])) {
|
||||
$has_attachments = false;
|
||||
@@ -959,7 +1060,7 @@ class Thread extends Model
|
||||
$content = null;
|
||||
$uploaded_file = null;
|
||||
|
||||
if (get_class($attachment) == 'Illuminate\Http\UploadedFile') {
|
||||
if (is_object($attachment) && get_class($attachment) == 'Illuminate\Http\UploadedFile') {
|
||||
|
||||
$uploaded_file = $attachment;
|
||||
$attachment = [];
|
||||
@@ -987,12 +1088,19 @@ class Thread extends Model
|
||||
}
|
||||
} else {
|
||||
// URL.
|
||||
$uploaded_file = \Helper::downloadRemoteFileAsTmpFile($attachment['file_url']);
|
||||
if (!$uploaded_file) {
|
||||
$file_path = \Helper::downloadRemoteFileAsTmp($attachment['file_url']);
|
||||
if (!$file_path) {
|
||||
continue;
|
||||
}
|
||||
$uploaded_file = new \Illuminate\Http\UploadedFile(
|
||||
$file_path, basename($file_path),
|
||||
null, null, true
|
||||
);
|
||||
if (empty($attachment['mime_type'])) {
|
||||
$attachment['mime_type'] = $uploaded_file->getClientMimeType();
|
||||
$attachment['mime_type'] = mime_content_type($file_path);
|
||||
if (empty($attachment['mime_type'])) {
|
||||
$attachment['mime_type'] = $uploaded_file->getMimeType();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1004,7 +1112,7 @@ class Thread extends Model
|
||||
$attachment['mime_type'],
|
||||
null,
|
||||
$content,
|
||||
$uploaded_file = $uploaded_file,
|
||||
$uploaded_file,
|
||||
$embedded = false,
|
||||
$thread->id,
|
||||
$user_id ?? null
|
||||
@@ -1018,6 +1126,10 @@ class Thread extends Model
|
||||
$thread->has_attachments = true;
|
||||
$conversation->has_attachments = true;
|
||||
}
|
||||
} else {
|
||||
$has_attachments = $data['has_attachments'] ?? false;
|
||||
$thread->has_attachments = $has_attachments;
|
||||
$conversation->has_attachments = $has_attachments;
|
||||
}
|
||||
|
||||
$thread->save();
|
||||
@@ -1039,30 +1151,61 @@ class Thread extends Model
|
||||
$conversation->setBcc($bcc);
|
||||
}
|
||||
|
||||
$update_folder = false;
|
||||
|
||||
if ($thread->isReply()) {
|
||||
$conversation->last_reply_at = $now;
|
||||
if ($is_customer) {
|
||||
$conversation->last_reply_from = Conversation::PERSON_CUSTOMER;
|
||||
// Reply from customer makes conversation active
|
||||
$conversation->status = Conversation::STATUS_ACTIVE;
|
||||
|
||||
// Set specific status
|
||||
if (!empty($data['status'])) {
|
||||
if ((int)$conversation->status != (int)$data['status']) {
|
||||
$update_folder = true;
|
||||
}
|
||||
$conversation->status = $data['status'];
|
||||
} else {
|
||||
if ((int)$conversation->status != Conversation::STATUS_ACTIVE) {
|
||||
$update_folder = true;
|
||||
}
|
||||
// Reply from customer makes conversation active
|
||||
$conversation->status = Conversation::STATUS_ACTIVE;
|
||||
}
|
||||
} else {
|
||||
$conversation->last_reply_from = Conversation::PERSON_USER;
|
||||
$conversation->user_updated_at = $now;
|
||||
$conversation->status = Conversation::STATUS_PENDING;
|
||||
|
||||
if (!empty($data['status'])) {
|
||||
if ((int)$conversation->status != (int)$data['status']) {
|
||||
$update_folder = true;
|
||||
}
|
||||
$conversation->status = $data['status'];
|
||||
} else {
|
||||
if ((int)$conversation->status != Conversation::STATUS_PENDING) {
|
||||
$update_folder = true;
|
||||
}
|
||||
// Reply from customer makes conversation active
|
||||
$conversation->status = Conversation::STATUS_PENDING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set specific status
|
||||
if (!empty($data['status'])) {
|
||||
$conversation->status = $data['status'];
|
||||
}
|
||||
|
||||
// Reply from customer to deleted conversation should undelete it.
|
||||
if ($data['type'] == Thread::TYPE_CUSTOMER && $conversation->state == Conversation::STATE_DELETED) {
|
||||
$conversation->state = Conversation::STATE_PUBLISHED;
|
||||
$update_folder = true;
|
||||
}
|
||||
|
||||
if ($update_conv) {
|
||||
$conversation->customer_id = $customer->id;
|
||||
|
||||
if ($is_customer) {
|
||||
$conversation->customer_email = $customer->getMainEmail();
|
||||
}
|
||||
|
||||
if ($update_folder) {
|
||||
$conversation->updateFolder();
|
||||
}
|
||||
}
|
||||
|
||||
// Update conversation here if needed.
|
||||
@@ -1182,6 +1325,27 @@ class Thread extends Model
|
||||
$this->setMetas($metas);
|
||||
}
|
||||
|
||||
public function getMetaFw($key, $default = null)
|
||||
{
|
||||
$meta = $this->getMeta($key, $default);
|
||||
if (!$meta) {
|
||||
$meta = $this->getMeta(self::$meta_fw_backward_compat[$key], $default);
|
||||
}
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset thread meta value.
|
||||
*/
|
||||
public function unsetMeta($key)
|
||||
{
|
||||
$metas = $this->getMetas();
|
||||
if (isset($metas[$key])) {
|
||||
unset($metas[$key]);
|
||||
$this->setMetas($metas);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name of the user who forwarded conversation.
|
||||
*/
|
||||
@@ -1212,7 +1376,7 @@ class Thread extends Model
|
||||
*/
|
||||
public function isForwarded()
|
||||
{
|
||||
if ($this->getMeta('forward_parent_conversation_id')) {
|
||||
if ($this->getMetaFw(self::META_FORWARD_PARENT_CONVERSATION_ID)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -1232,7 +1396,7 @@ class Thread extends Model
|
||||
*/
|
||||
public function getForwardParentConversation()
|
||||
{
|
||||
return Conversation::where('id', $this->getMeta('forward_parent_conversation_id'))
|
||||
return Conversation::where('id', $this->getMetaFw(self::META_FORWARD_PARENT_CONVERSATION_ID))
|
||||
->rememberForever()
|
||||
->first();
|
||||
}
|
||||
@@ -1242,7 +1406,7 @@ class Thread extends Model
|
||||
*/
|
||||
public function getForwardChildConversation()
|
||||
{
|
||||
return Conversation::where('id', $this->getMeta('forward_child_conversation_id'))
|
||||
return Conversation::where('id', $this->getMetaFw(self::META_FORWARD_CHILD_CONVERSATION_ID))
|
||||
->first();
|
||||
}
|
||||
|
||||
@@ -1251,7 +1415,23 @@ class Thread extends Model
|
||||
*/
|
||||
public function fetchBody()
|
||||
{
|
||||
$message = \MailHelper::fetchMessage($this->conversation->mailbox, $this->message_id);
|
||||
$mailbox = null;
|
||||
|
||||
// The conversation may has been moved from another mailbox.
|
||||
if (!empty($this->conversation->meta['orig_mailbox_id'])) {
|
||||
$mailbox = Mailbox::find($this->conversation->meta['orig_mailbox_id']);
|
||||
}
|
||||
|
||||
if (!$mailbox) {
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
}
|
||||
$message = \MailHelper::fetchMessage($mailbox, $this->message_id, $this->getMailDate());
|
||||
|
||||
// Try without limiting by date.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3658
|
||||
if (!$message) {
|
||||
$message = \MailHelper::fetchMessage($mailbox, $this->message_id);
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
return '';
|
||||
@@ -1266,6 +1446,22 @@ class Thread extends Model
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function parseHeaders()
|
||||
{
|
||||
return \MailHelper::parseHeaders($this->headers);
|
||||
}
|
||||
|
||||
public function getMailDate()
|
||||
{
|
||||
$data = $this->parseHeaders();
|
||||
|
||||
if (empty($data->date)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \Helper::parseDateToCarbon($data->date);
|
||||
}
|
||||
|
||||
public function getActionTypeName()
|
||||
{
|
||||
if (!$this->action_type) {
|
||||
@@ -1276,4 +1472,124 @@ class Thread extends Model
|
||||
|
||||
return self::$action_types[$this->action_type] ?? '';
|
||||
}
|
||||
|
||||
public function isCustomerMessage()
|
||||
{
|
||||
return $this->type == self::TYPE_CUSTOMER;
|
||||
}
|
||||
|
||||
public function isUserMessage()
|
||||
{
|
||||
return $this->type == self::TYPE_MESSAGE;
|
||||
}
|
||||
|
||||
public static function replaceBase64ImagesWithAttachments($body, $user_id = null)
|
||||
{
|
||||
\Helper::setPcreBacktrackLimit();
|
||||
|
||||
$body = preg_replace_callback("#(<img[^<>]+src=[\"'])data:image/([^;]+);base64,([^\"']+)([\"'])#",
|
||||
function ($match) {
|
||||
$attachment = null;
|
||||
$data = base64_decode($match[3]);
|
||||
|
||||
if ($data) {
|
||||
$attachment = Attachment::create(
|
||||
$file_name = number_format(microtime(true), 4, '', '').'.'.$match[2],
|
||||
$mime_type = 'image/'.$match[2],
|
||||
$type = Attachment::TYPE_IMAGE,
|
||||
$data,
|
||||
$uploaded_file = null,
|
||||
$embedded = true,
|
||||
$thread_id = null,
|
||||
$user_id = \Auth::id()
|
||||
);
|
||||
}
|
||||
if ($attachment) {
|
||||
return $match[1].$attachment->url().$match[4];
|
||||
} else {
|
||||
return $match[0];
|
||||
}
|
||||
}, $body
|
||||
);
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function getMessageId($mailbox = null)
|
||||
{
|
||||
if ($this->isCustomerMessage() && $this->message_id) {
|
||||
return $this->message_id;
|
||||
}
|
||||
if ($this->isUserMessage()) {
|
||||
if (!$mailbox) {
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
}
|
||||
return \MailHelper::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->id.'-'.\MailHelper::getMessageIdHash($this->id).'@'.$mailbox->getEmailDomain();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sorts threads in desc order by created_at and ID.
|
||||
//
|
||||
// Threads has to be sorted by created_at and not by id.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/2938
|
||||
// Sometimes thread.created_at may be the same,
|
||||
// in such cases we also need to sort by thread ID.
|
||||
public static function sortThreads($threads)
|
||||
{
|
||||
return $threads->sort(function ($a, $b) {
|
||||
$a_ts = $a->created_at->getTimestamp();
|
||||
$b_ts = $b->created_at->getTimestamp();
|
||||
if ($a_ts == $b_ts) {
|
||||
if ($a->id < $b->id) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
return ($a_ts < $b_ts) ? 1 : -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function getLastThread($threads)
|
||||
{
|
||||
$threads = self::sortThreads($threads);
|
||||
return $threads->first();
|
||||
}
|
||||
|
||||
public function canRetrySend()
|
||||
{
|
||||
if ($this->isSendStatusSuccess()) {
|
||||
return false;
|
||||
}
|
||||
// Check if failed_job still exists.
|
||||
if (!$this->getFailedJobId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isSendStatusSuccess()
|
||||
{
|
||||
// We have not tried to send the email yet.
|
||||
if ((int)$this->send_status == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($this->send_status, [SendLog::STATUS_SEND_ERROR, SendLog::STATUS_DELIVERY_ERROR, SendLog::STATUS_SEND_INTERMEDIATE_ERROR])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getFailedJobId()
|
||||
{
|
||||
return \App\FailedJob::where('queue', 'emails')
|
||||
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%{i:0;i:'.$this->id.';%')
|
||||
->value('id');
|
||||
}
|
||||
}
|
||||
|
||||
264
app/User.php
264
app/User.php
@@ -6,6 +6,8 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Email;
|
||||
use App\Follower;
|
||||
use App\Mail\PasswordChanged;
|
||||
use App\Mail\UserInvite;
|
||||
use App\Notifications\WebsiteNotification;
|
||||
@@ -26,11 +28,14 @@ class User extends Authenticatable
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
const PHOTO_DIRECTORY = 'users';
|
||||
const PHOTO_SIZE = 50; // px
|
||||
const PHOTO_QUALITY = 77;
|
||||
|
||||
const EMAIL_MAX_LENGTH = 100;
|
||||
|
||||
const EMAIL_DELETED_SUFFIX = '_deleted';
|
||||
|
||||
const DEFAULT_TIMEZONE = 'UTC';
|
||||
|
||||
/**
|
||||
* Roles.
|
||||
*/
|
||||
@@ -46,7 +51,7 @@ class User extends Authenticatable
|
||||
* Types.
|
||||
*/
|
||||
const TYPE_USER = 1;
|
||||
const TYPE_TEAM = 2;
|
||||
const TYPE_ROBOT = 2; // Workflows, teams, etc.
|
||||
|
||||
/**
|
||||
* Statuses.
|
||||
@@ -111,12 +116,20 @@ class User extends Authenticatable
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'role', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts', 'locale'];
|
||||
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts', 'locale'];
|
||||
|
||||
protected $casts = [
|
||||
'permissions' => 'array',
|
||||
];
|
||||
|
||||
public function __construct(array $attributes = array())
|
||||
{
|
||||
$this->setRawAttributes(array_merge($this->attributes, array(
|
||||
'timezone' => config('app.timezone') ?: User::DEFAULT_TIMEZONE
|
||||
)), true);
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* For array_unique function.
|
||||
*
|
||||
@@ -208,7 +221,7 @@ class User extends Authenticatable
|
||||
*/
|
||||
public function getFullName()
|
||||
{
|
||||
return $this->first_name.' '.$this->last_name;
|
||||
return \Eventy::filter('user.full_name', $this->first_name.' '.$this->last_name, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,6 +306,7 @@ class User extends Authenticatable
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IDs of mailboxes to which user has access.
|
||||
*/
|
||||
@@ -321,7 +335,7 @@ class User extends Authenticatable
|
||||
//$mailboxes = $this->mailboxesCanViewWithSettings(true);
|
||||
$mailboxes = $this->mailboxesSettings();
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
if (!empty(json_decode($mailbox->access))) {
|
||||
if (!empty($mailbox->access) && !empty(json_decode($mailbox->access))) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -338,13 +352,32 @@ class User extends Authenticatable
|
||||
} else {
|
||||
//$mailbox = $this->mailboxesCanViewWithSettings(true)->where('id', $mailbox_id)->first();
|
||||
$mailbox = $this->mailboxesSettings()->where('mailbox_id', $mailbox_id)->first();
|
||||
if ($mailbox && !empty(json_decode($mailbox->access))) {
|
||||
if ($mailbox && !empty(json_decode($mailbox->access ?? ''))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to check if user has some exta access permission
|
||||
* for a given mailbox.
|
||||
*/
|
||||
public function hasManageMailboxPermission($mailbox_id, $perm) {
|
||||
// Experimental feature.
|
||||
// This option does not affect admin users.
|
||||
if ($perm == Mailbox::ACCESS_PERM_ASSIGNED) {
|
||||
if ($this->isAdmin()) {
|
||||
return false;
|
||||
} else {
|
||||
$show_only_assigned_conversations = config('app.show_only_assigned_conversations') ?? '';
|
||||
if (in_array($this->id, explode(',', $show_only_assigned_conversations))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -352,6 +385,8 @@ class User extends Authenticatable
|
||||
$mailbox = $this->mailboxesSettings()->where('mailbox_id', $mailbox_id)->first();
|
||||
if ($mailbox && !empty($mailbox->access) && in_array($perm, json_decode($mailbox->access))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,6 +405,21 @@ class User extends Authenticatable
|
||||
return str_random($length);
|
||||
}
|
||||
|
||||
/**
|
||||
* This password indicates that the user has not set the password by himself.
|
||||
*/
|
||||
public static function getDummyPassword()
|
||||
{
|
||||
return encrypt('dummy_'.str_random(8));
|
||||
}
|
||||
|
||||
public function isDummyPassword()
|
||||
{
|
||||
$decrypted_password = \Helper::decrypt($this->password);
|
||||
return preg_match("#^dummy_#", $decrypted_password);
|
||||
//return Hash::check($this->getDummyPassword(), $this->password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL for editing user.
|
||||
*
|
||||
@@ -421,11 +471,11 @@ class User extends Authenticatable
|
||||
continue;
|
||||
}
|
||||
foreach (Folder::$personal_types as $type) {
|
||||
$folder = new Folder();
|
||||
$folder->mailbox_id = $mailbox_id;
|
||||
$folder->user_id = $this->id;
|
||||
$folder->type = $type;
|
||||
$folder->save();
|
||||
Folder::create([
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'user_id' => $this->id,
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,7 +488,7 @@ class User extends Authenticatable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function dateFormat($date, $format = 'M j, Y H:i', $user = null, $modify_format = true)
|
||||
public static function dateFormat($date, $format = 'M j, Y H:i', $user = null, $modify_format = true, $use_user_timezone = true)
|
||||
{
|
||||
if (!$user) {
|
||||
$user = auth()->user();
|
||||
@@ -456,6 +506,10 @@ class User extends Authenticatable
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$format) {
|
||||
$format = 'M j, Y H:i';
|
||||
}
|
||||
|
||||
if ($user && $user !== false) {
|
||||
if ($modify_format) {
|
||||
if ($user->time_format == self::TIME_FORMAT_12) {
|
||||
@@ -463,6 +517,7 @@ class User extends Authenticatable
|
||||
'H' => 'h',
|
||||
'G' => 'g',
|
||||
':i' => ':ia',
|
||||
':i:s' => ':i:sa',
|
||||
':ia:s' => ':i:sa',
|
||||
]);
|
||||
} else {
|
||||
@@ -476,7 +531,40 @@ class User extends Authenticatable
|
||||
}
|
||||
// todo: formatLocalized has to be used here and below,
|
||||
// but it returns $format value instead of formatted date
|
||||
return $date->setTimezone($user->timezone)->format($format);
|
||||
if ($use_user_timezone) {
|
||||
$date->setTimezone($user->timezone);
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists('IntlDateFormatter')) {
|
||||
|
||||
// Convert `strftime` format to `IntlDateFormatter` pattern.
|
||||
// https://unicode-org.github.io/icu/userguide/format_parse/datetime/
|
||||
$format = strtr($format, [
|
||||
'M' => 'MMM',
|
||||
'm' => 'MM',
|
||||
'j' => 'd',
|
||||
'd' => 'dd',
|
||||
'H' => 'HH',
|
||||
'h' => 'hh',
|
||||
'i' => 'mm',
|
||||
's' => 'ss',
|
||||
'l' => 'cccc',
|
||||
'O' => 'xx',
|
||||
]);
|
||||
|
||||
// Remove dot from month name.
|
||||
$formatted = $date->formatLocalized($format);
|
||||
if (!strstr($format, '.')) {
|
||||
$formatted = str_replace('.', '', $formatted);
|
||||
}
|
||||
|
||||
// AM/PM to am/pm.
|
||||
$formatted = preg_replace_callback('#\d+(AM|PM)$#', function ($m) {
|
||||
return strtolower($m[0] ?? '');
|
||||
}, $formatted);
|
||||
|
||||
return \Helper::mbUcfirst($formatted);
|
||||
} else {
|
||||
return $date->format($format);
|
||||
}
|
||||
@@ -547,7 +635,7 @@ class User extends Authenticatable
|
||||
public static function getUserPermissionName($user_permission)
|
||||
{
|
||||
$user_permission_names = [
|
||||
self::PERM_DELETE_CONVERSATIONS => __('Users are allowed to delete notes/conversations'),
|
||||
self::PERM_DELETE_CONVERSATIONS => __('Users are allowed to delete conversations'),
|
||||
self::PERM_EDIT_CONVERSATIONS => __('Users are allowed to edit notes/replies'),
|
||||
self::PERM_EDIT_SAVED_REPLIES => __('Users are allowed to edit/delete saved replies'),
|
||||
self::PERM_EDIT_TAGS => __('Users are allowed to manage tags'),
|
||||
@@ -624,7 +712,7 @@ class User extends Authenticatable
|
||||
saveToSendLog($this, SendLog::STATUS_SEND_ERROR);
|
||||
|
||||
if ($throw_exceptions) {
|
||||
throw new \Exception(__('Error occured sending email to :email. Please check logs for more details.', ['email' => $this->email]), 1);
|
||||
throw new \Exception(__('Error occurred sending email to :email. Please check logs for more details.', ['email' => $this->email]), 1);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -643,9 +731,12 @@ class User extends Authenticatable
|
||||
/**
|
||||
* Generate and set password.
|
||||
*/
|
||||
public function setPassword()
|
||||
public function setPassword($password = null)
|
||||
{
|
||||
$this->password = Hash::make($this->generateRandomPassword());
|
||||
if ($password === null) {
|
||||
$password = $this->generateRandomPassword();
|
||||
}
|
||||
$this->password = Hash::make($password);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -776,10 +867,14 @@ class User extends Authenticatable
|
||||
{
|
||||
$real_path = $uploaded_file;
|
||||
if (!is_string($uploaded_file)) {
|
||||
$real_path = $uploaded_file->getRealPath();
|
||||
// Fallback to getPathname() for Windows.
|
||||
// https://github.com/freescout-help-desk/freescout/issues/4105
|
||||
$real_path = $uploaded_file->getRealPath() ?: $uploaded_file->getPathname();
|
||||
$mime_type = $uploaded_file->getMimeType();
|
||||
}
|
||||
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, self::PHOTO_SIZE, self::PHOTO_SIZE);
|
||||
|
||||
$photo_size = config('app.user_photo_size');
|
||||
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, $photo_size, $photo_size);
|
||||
|
||||
if (!$resized_image) {
|
||||
return false;
|
||||
@@ -865,7 +960,7 @@ class User extends Authenticatable
|
||||
*/
|
||||
public static function getSuperAdmin()
|
||||
{
|
||||
return self::where('role', self::ROLE_ADMIN)->first();
|
||||
return self::nonDeleted()->where('role', self::ROLE_ADMIN)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -879,10 +974,7 @@ class User extends Authenticatable
|
||||
return null;
|
||||
}
|
||||
|
||||
$user->fill($data);
|
||||
|
||||
$user->password = \Hash::make($data['password']);
|
||||
$user->email = Email::sanitizeEmail($data['email']);
|
||||
$user->setData($data);
|
||||
|
||||
try {
|
||||
$user->save();
|
||||
@@ -893,6 +985,36 @@ class User extends Authenticatable
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fields.
|
||||
*/
|
||||
public function setData($data, $replace_data = true, $save = false)
|
||||
{
|
||||
if (isset($data['email'])) {
|
||||
$data['email'] = Email::sanitizeEmail($data['email']);
|
||||
}
|
||||
if (isset($data['password']) && empty($data['no_password_hashing'])) {
|
||||
$data['password'] = \Hash::make($data['password']);
|
||||
}
|
||||
|
||||
if ($replace_data) {
|
||||
$this->fill($data);
|
||||
} else {
|
||||
// Update empty fields.
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array($key, $this->fillable) && empty($this->$key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('user.set_data', $this, $data, $replace_data);
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user's role is higher than passed.
|
||||
*/
|
||||
@@ -935,12 +1057,15 @@ class User extends Authenticatable
|
||||
|
||||
/**
|
||||
* Get query to fetch non-deleted users.
|
||||
* Some modules may extend this condition, to allow this user $extended parameter.
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function nonDeleted()
|
||||
public static function nonDeleted($extended = false)
|
||||
{
|
||||
return self::where('status', '!=', self::STATUS_DELETED);
|
||||
$condition = self::where('status', '!=', self::STATUS_DELETED);
|
||||
|
||||
return \Eventy::filter('user.non_deleted_condition', $condition, $extended);
|
||||
}
|
||||
|
||||
public function isActive()
|
||||
@@ -979,6 +1104,7 @@ class User extends Authenticatable
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
})
|
||||
->whereIn('mailbox_user.mailbox_id', $mailbox_ids)
|
||||
->groupBy('users.id')
|
||||
->get();
|
||||
}
|
||||
|
||||
@@ -1003,12 +1129,12 @@ class User extends Authenticatable
|
||||
|
||||
public function getAuthToken()
|
||||
{
|
||||
return md5($this->id.config('app.key'));
|
||||
return md5($this->id.''.$this->created_at.config('app.key'));
|
||||
}
|
||||
|
||||
public static function findNonDeleted($id)
|
||||
public static function findNonDeleted($id, $extended = false)
|
||||
{
|
||||
return User::nonDeleted()->where('id', $id)->first();
|
||||
return User::nonDeleted($extended)->where('id', $id)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1028,4 +1154,84 @@ class User extends Authenticatable
|
||||
{
|
||||
return \Eventy::filter('user_permissions.list', self::$user_permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user main and alternate emails.
|
||||
*/
|
||||
public function hasEmail($email)
|
||||
{
|
||||
$email = Email::sanitizeEmail($email);
|
||||
|
||||
if ($this->email == $email) {
|
||||
return true;
|
||||
}
|
||||
$alt_emails = explode(',', $this->emails ?? '');
|
||||
|
||||
foreach ($alt_emails as $alt_email) {
|
||||
if (Email::sanitizeEmail($alt_email) == $email) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a mailbox with specified email.
|
||||
*/
|
||||
public static function mailboxEmailExists($email)
|
||||
{
|
||||
$email = Email::sanitizeEmail($email);
|
||||
$mailbox = Mailbox::where('email', $email)->first();
|
||||
|
||||
if ($mailbox) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function followConversation($conversation_id)
|
||||
{
|
||||
try {
|
||||
$follower = new Follower();
|
||||
$follower->conversation_id = $conversation_id;
|
||||
$follower->user_id = $this->id;
|
||||
$follower->save();
|
||||
} catch (\Exception $e) {
|
||||
// Already exists
|
||||
}
|
||||
}
|
||||
|
||||
// If there will be some issues, extra "robot" field
|
||||
// may need to be added to Users table.
|
||||
public static function getRobotsCondition()
|
||||
{
|
||||
return User::where('type', User::TYPE_ROBOT);
|
||||
}
|
||||
|
||||
// Truncate fields to their max lengths to avoid PostgreSQL error:
|
||||
// SQLSTATE[22001]: String data, right truncated: 7 ERROR: value too long for type character varying(100).
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3489
|
||||
public function setFirstNameAttribute($first_name)
|
||||
{
|
||||
$this->attributes['first_name'] = mb_substr($first_name ?? '', 0, 20);
|
||||
}
|
||||
public function setLastNameAttribute($last_name)
|
||||
{
|
||||
$this->attributes['last_name'] = mb_substr($last_name ?? '', 0, 30);
|
||||
}
|
||||
public function setEmailAttribute($email)
|
||||
{
|
||||
$this->attributes['email'] = mb_substr($email ?? '', 0, 100);
|
||||
}
|
||||
public function setJobTitleAttribute($job_title)
|
||||
{
|
||||
$this->attributes['job_title'] = mb_substr($job_title ?? '', 0, 100);
|
||||
}
|
||||
|
||||
public function canSeeOnlyAssignedConversations()
|
||||
{
|
||||
return $this->hasManageMailboxPermission(0, Mailbox::ACCESS_PERM_ASSIGNED);
|
||||
}
|
||||
}
|
||||
|
||||
19
artisan
19
artisan
@@ -9,6 +9,25 @@ if (!version_compare(phpversion(), '7.0.0', '>=')) {
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// PHP 8.1 fix.
|
||||
if (! function_exists('e')) {
|
||||
/**
|
||||
* Escape HTML special characters in a string.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Support\Htmlable|string $value
|
||||
* @param bool $doubleEncode
|
||||
* @return string
|
||||
*/
|
||||
function e($value, $doubleEncode = false)
|
||||
{
|
||||
if ($value instanceof \Illuminate\Contracts\Support\Htmlable) {
|
||||
return $value->toHtml();
|
||||
}
|
||||
|
||||
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8', $doubleEncode);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register The Auto Loader
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user