Compare commits
1105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4c69e3a9 | ||
|
|
d961797c40 | ||
|
|
b8dd8d30a6 | ||
|
|
213fb80f95 | ||
|
|
7531eb4d64 | ||
|
|
f7c9d08b6c | ||
|
|
31995613ba | ||
|
|
529df15375 | ||
|
|
abfbf9959a | ||
|
|
9caa1e10cc | ||
|
|
81139ab8b2 | ||
|
|
5f0510484e | ||
|
|
c87b3a97d7 | ||
|
|
74712cf2d9 | ||
|
|
40e0776219 | ||
|
|
dc1d3ec5da | ||
|
|
d72bc81563 | ||
|
|
4d3bddc4d2 | ||
|
|
3cdbeaa768 | ||
|
|
13a9ca2ff3 | ||
|
|
53f6c18cbe | ||
|
|
3211037aba | ||
|
|
f5bf5afe14 | ||
|
|
4f65bbc723 | ||
|
|
56e8e3862b | ||
|
|
0d53e23151 | ||
|
|
dcdc372e45 | ||
|
|
ca847752df | ||
|
|
6156dc0824 | ||
|
|
04374dccdf | ||
|
|
1e4e67cc37 | ||
|
|
338df4aa44 | ||
|
|
fc24702844 | ||
|
|
fb82f16ef6 | ||
|
|
d915c646bb | ||
|
|
400ec40ff1 | ||
|
|
d73f64b669 | ||
|
|
403a160917 | ||
|
|
3904e6d120 | ||
|
|
bcdc2e793e | ||
|
|
cc68ee3d27 | ||
|
|
0b0ad5067c | ||
|
|
b391d4496c | ||
|
|
31df77a0b9 | ||
|
|
5e053d2954 | ||
|
|
3cfa02fde2 | ||
|
|
e3a5ac1432 | ||
|
|
97abf1da76 | ||
|
|
1a28d142f2 | ||
|
|
6dcd0dc8cc | ||
|
|
23d812cdd4 | ||
|
|
90270ad31f | ||
|
|
5ed8d90c70 | ||
|
|
1f2182d014 | ||
|
|
c1a2b819ec | ||
|
|
2b8e3fda63 | ||
|
|
8bb0988010 | ||
|
|
5c4eaa17cf | ||
|
|
4bb44a8d1a | ||
|
|
826418d5e4 | ||
|
|
c2810be1b8 | ||
|
|
3243d57b01 | ||
|
|
9bf1637320 | ||
|
|
ae415033dd | ||
|
|
41d2743816 | ||
|
|
2e6eb810cd | ||
|
|
7f99ef2c74 | ||
|
|
fda79460b8 | ||
|
|
b91155c2ce | ||
|
|
83e2b0af7f | ||
|
|
d82b3dc896 | ||
|
|
7f732bcfef | ||
|
|
32a6c46e8a | ||
|
|
097615ef69 | ||
|
|
ebaddd76a7 | ||
|
|
aa4105e0cd | ||
|
|
487e0bf370 | ||
|
|
8121c489d5 | ||
|
|
3362bbeb45 | ||
|
|
08796da55e | ||
|
|
b68b17c3df | ||
|
|
218605334c | ||
|
|
8d9f9bead8 | ||
|
|
86e1c292f2 | ||
|
|
b4c334ed9c | ||
|
|
3b3c23451b | ||
|
|
8f71f77b1b | ||
|
|
84f53fd682 | ||
|
|
a3a27259fc | ||
|
|
eff6c8c351 | ||
|
|
2d21c21256 | ||
|
|
cf01a524e5 | ||
|
|
51e2f71afc | ||
|
|
358781905f | ||
|
|
6b782ede62 | ||
|
|
425080093b | ||
|
|
415b20020a | ||
|
|
29b36472c9 | ||
|
|
5f6bd2d993 | ||
|
|
8fb6d9152f | ||
|
|
1884048f9c | ||
|
|
1d859d3fb9 | ||
|
|
b41e78e71e | ||
|
|
e8afbd85a9 | ||
|
|
d9a8fc8e8b | ||
|
|
a15b65f819 | ||
|
|
7dc35d143a | ||
|
|
0bdb22e934 | ||
|
|
09b777181a | ||
|
|
18217c58b6 | ||
|
|
147926bcf1 | ||
|
|
3c904fc048 | ||
|
|
d1736d9f09 | ||
|
|
8ef1fd811b | ||
|
|
a64cfa6307 | ||
|
|
3f80845371 | ||
|
|
80382199ee | ||
|
|
292e9ec0ae | ||
|
|
59b57e268d | ||
|
|
153e68f7ae | ||
|
|
0b3d36cee0 | ||
|
|
ce2557dd24 | ||
|
|
a8f5835a39 | ||
|
|
9a273473d5 | ||
|
|
25b0c19db1 | ||
|
|
bbc3e7f3aa | ||
|
|
8053591031 | ||
|
|
3d2fcd020d | ||
|
|
3d7fd68fa8 | ||
|
|
1a011d9ea4 | ||
|
|
57a943edec | ||
|
|
85e9dc53f2 | ||
|
|
a682d8f7df | ||
|
|
bdc1073a28 | ||
|
|
ac79c57de4 | ||
|
|
1b70e45a1c | ||
|
|
97d0c470fb | ||
|
|
c92589da2a | ||
|
|
3dffd231ae | ||
|
|
f429bdef8e | ||
|
|
11f223ab04 | ||
|
|
2ccf407bd9 | ||
|
|
a172d476f5 | ||
|
|
f37303e8a0 | ||
|
|
60016fd532 | ||
|
|
ab3de1f60b | ||
|
|
6014509dd2 | ||
|
|
4e856ea783 | ||
|
|
23aeefbcbd | ||
|
|
b84afb99a5 | ||
|
|
2dada15292 | ||
|
|
a87e2199f0 | ||
|
|
0e85c73a77 | ||
|
|
d7ef54a37e | ||
|
|
ed47a5e3c1 | ||
|
|
27698ebea0 | ||
|
|
3bcdcd9284 | ||
|
|
640379e4d7 | ||
|
|
47b4dfb4b8 | ||
|
|
da2ea1cd1b | ||
|
|
12e80d60b6 | ||
|
|
141fd8320e | ||
|
|
a9ac930cc0 | ||
|
|
344f06178f | ||
|
|
0f03f4fffd | ||
|
|
24ef816c2e | ||
|
|
516e02be1d | ||
|
|
3c13551a7a | ||
|
|
efcf05ad8b | ||
|
|
26b7e1c5c3 | ||
|
|
2e71180f7c | ||
|
|
a42e0db899 | ||
|
|
3c7dda7118 | ||
|
|
740d8aa706 | ||
|
|
e82a1c2b81 | ||
|
|
7ea7795913 | ||
|
|
2589aa1da9 | ||
|
|
18295e83fb | ||
|
|
768c4561a9 | ||
|
|
dcb7e7715f | ||
|
|
767b20f059 | ||
|
|
66fba5f20a | ||
|
|
921186e2e0 | ||
|
|
5d41b6908c | ||
|
|
fede8d633a | ||
|
|
182f49f758 | ||
|
|
91c914fd14 | ||
|
|
c6ea0a85cb | ||
|
|
0a6e0a100c | ||
|
|
d4b81c4cdb | ||
|
|
2b4f67befe | ||
|
|
56597bbd29 | ||
|
|
c0fc874ce4 | ||
|
|
d26ab69b18 | ||
|
|
346148ae60 | ||
|
|
8e9bcc6040 | ||
|
|
f4c76467bb | ||
|
|
26a8d8511f | ||
|
|
1873dc6a20 | ||
|
|
8fa94d8452 | ||
|
|
d62088fa1f | ||
|
|
03d8e2e0e8 | ||
|
|
4397e0c756 | ||
|
|
42b23f6e3a | ||
|
|
501b0719cc | ||
|
|
1a7289a395 | ||
|
|
6153db71c8 | ||
|
|
5a7eae76d7 | ||
|
|
cc8e5da17b | ||
|
|
54d44ba87a | ||
|
|
64c46e3941 | ||
|
|
2458539c6f | ||
|
|
c56b9a0573 | ||
|
|
eeb00763d3 | ||
|
|
800a484acc | ||
|
|
b3e444be47 | ||
|
|
ff025b94d2 | ||
|
|
96986e8a22 | ||
|
|
2d0c72d5bb | ||
|
|
384ff40d88 | ||
|
|
8dca28adeb | ||
|
|
1f6a95c4a7 | ||
|
|
3493f16035 | ||
|
|
97d12be54d | ||
|
|
19d84bb479 | ||
|
|
2a29e3194e | ||
|
|
4b8904f335 | ||
|
|
a9d43435e9 | ||
|
|
9884a767ff | ||
|
|
aa1dba6241 | ||
|
|
ba9293a598 | ||
|
|
7a15e55da2 | ||
|
|
77a851cbe0 | ||
|
|
595eaa7a20 | ||
|
|
a66b6d019e | ||
|
|
b6257aa21c | ||
|
|
1a46c7f001 | ||
|
|
8ddbe18543 | ||
|
|
5c8de33eb6 | ||
|
|
7c663cacf8 | ||
|
|
1bedf8394d | ||
|
|
df0ca8b740 | ||
|
|
ced188f286 | ||
|
|
96fa6b5507 | ||
|
|
c19600bc7c | ||
|
|
cbddefcda2 | ||
|
|
a06d2f4c81 | ||
|
|
929fc80e66 | ||
|
|
56f2f1053b | ||
|
|
3f1ec34dea | ||
|
|
ddc4f9d452 | ||
|
|
14ae10536f | ||
|
|
a9a3863d05 | ||
|
|
9e8ebe17ea | ||
|
|
9cca472d29 | ||
|
|
44390a6280 | ||
|
|
345cdb2702 | ||
|
|
a2adf76962 | ||
|
|
3d477f6296 | ||
|
|
b0469ed5bc | ||
|
|
63f5b92a3c | ||
|
|
a5d03df940 | ||
|
|
2b07bcbca7 | ||
|
|
d55bfd6495 | ||
|
|
8210891675 | ||
|
|
beeeadf34a | ||
|
|
1f8b2098cc | ||
|
|
669ebf79e1 | ||
|
|
8a0077c94d | ||
|
|
a2910deed3 | ||
|
|
039623097d | ||
|
|
833158b0de | ||
|
|
d1598d61ac | ||
|
|
672149d3f7 | ||
|
|
5db4c5ced2 | ||
|
|
8f07621946 | ||
|
|
3235e60f72 | ||
|
|
566640d287 | ||
|
|
c10686e546 | ||
|
|
fb1ac61d64 | ||
|
|
813bc84575 | ||
|
|
323e9d7a20 | ||
|
|
f134a0288e | ||
|
|
7324f15c50 | ||
|
|
b8286e7875 | ||
|
|
1afb66ce0b | ||
|
|
ec4670757e | ||
|
|
92a9e7cb78 | ||
|
|
dd8e8274a1 | ||
|
|
b010996391 | ||
|
|
5bbe2b9d73 | ||
|
|
c2336d9ca8 | ||
|
|
1b29571ba9 | ||
|
|
810d2e3924 | ||
|
|
55d24994df | ||
|
|
f0bba4b77b | ||
|
|
eefc7fd153 | ||
|
|
f24b2e5040 | ||
|
|
d96c180631 | ||
|
|
f78933e2a4 | ||
|
|
b30b996e9c | ||
|
|
a51cb49225 | ||
|
|
f4924c3140 | ||
|
|
c1b08383ef | ||
|
|
59651ee24d | ||
|
|
11b79b2b48 | ||
|
|
ac03db6380 | ||
|
|
5d590d598e | ||
|
|
17a157eb60 | ||
|
|
2446a94d50 | ||
|
|
53240b6360 | ||
|
|
b4b09213f7 | ||
|
|
cdf6fd07cf | ||
|
|
a0c5748c59 | ||
|
|
d599c63816 | ||
|
|
c4fb834cd0 | ||
|
|
f605cf7e85 | ||
|
|
fa06b7148e | ||
|
|
f17755aad8 | ||
|
|
e4fdab0a19 | ||
|
|
94af31c686 | ||
|
|
28902b4c9f | ||
|
|
e57ed73f98 | ||
|
|
bf1eb5985a | ||
|
|
2188ce8a4e | ||
|
|
33767a590a | ||
|
|
352469b662 | ||
|
|
01b8da7cc2 | ||
|
|
f9b517c849 | ||
|
|
962418a9ee | ||
|
|
3353d57c84 | ||
|
|
0ed7db3b0c | ||
|
|
5ad58e5b25 | ||
|
|
e28f174f20 | ||
|
|
4a8aae7aff | ||
|
|
e780a863cf | ||
|
|
86c2724ceb | ||
|
|
6677cf39a9 | ||
|
|
0bac1ae3c4 | ||
|
|
a152d8bf06 | ||
|
|
badba682a0 | ||
|
|
90ae9e2a7e | ||
|
|
69cbede2f4 | ||
|
|
754c3d7e75 | ||
|
|
3814a2d3e9 | ||
|
|
97fb5678bc | ||
|
|
964249c1d4 | ||
|
|
607ae55db7 | ||
|
|
b7dae29b5f | ||
|
|
87d48a3428 | ||
|
|
82a8876209 | ||
|
|
325ad1914b | ||
|
|
d1bcef4df7 | ||
|
|
7c0ef24bfd | ||
|
|
038c3610ac | ||
|
|
62ea562f26 | ||
|
|
3f17e9b811 | ||
|
|
c08c077b8f | ||
|
|
b5efc28b0f | ||
|
|
ab55a5fe19 | ||
|
|
f5e493e5dc | ||
|
|
e6ab5eba07 | ||
|
|
d0f1b621fe | ||
|
|
79aca06be8 | ||
|
|
9e7f2e22dd | ||
|
|
d11c9c83f0 | ||
|
|
9e0622f91f | ||
|
|
ea7e30b9cc | ||
|
|
9c52ea8eca | ||
|
|
79db2920ca | ||
|
|
df9370a37f | ||
|
|
416d07fcce | ||
|
|
5384a32d58 | ||
|
|
18e9679553 | ||
|
|
c0cef1b178 | ||
|
|
045c1833e3 | ||
|
|
d4f8134de4 | ||
|
|
78596eb4c9 | ||
|
|
380a275248 | ||
|
|
a0b98cabf1 | ||
|
|
2ccc6dd141 | ||
|
|
a52336dc01 | ||
|
|
d70ae69dae | ||
|
|
5eeed269b6 | ||
|
|
eedc9e8f94 | ||
|
|
950af7b268 | ||
|
|
5f9e02012a | ||
|
|
548517b906 | ||
|
|
c29f902483 | ||
|
|
46919ba5a3 | ||
|
|
c4a4f33633 | ||
|
|
5176f0456f | ||
|
|
fb3e2635f5 | ||
|
|
380d1269d0 | ||
|
|
069e82c097 | ||
|
|
9a836a01fd | ||
|
|
eaffa01452 | ||
|
|
169fa21966 | ||
|
|
20e0dae7a3 | ||
|
|
358db187be | ||
|
|
fe17e3c4bb | ||
|
|
60e0d3c40c | ||
|
|
1753a6f654 | ||
|
|
d41e0e6c8c | ||
|
|
6f09d91a50 | ||
|
|
75c6008acc | ||
|
|
7c581cc7bb | ||
|
|
0715f008ca | ||
|
|
61f4c3760b | ||
|
|
ad1a464335 | ||
|
|
76ca9a1153 | ||
|
|
461a786ebc | ||
|
|
170c282f3e | ||
|
|
c7480c81b2 | ||
|
|
de62a0c11b | ||
|
|
dc35f63586 | ||
|
|
f5d7808792 | ||
|
|
1bf03b619a | ||
|
|
e109f5782c | ||
|
|
3c57cf0f65 | ||
|
|
475917c110 | ||
|
|
3b7ece1790 | ||
|
|
2a6c5faa79 | ||
|
|
7446cce387 | ||
|
|
15bb477e84 | ||
|
|
83071f3a1d | ||
|
|
eb579823db | ||
|
|
fe702bcaea | ||
|
|
0d87a6ea93 | ||
|
|
d9135c134b | ||
|
|
eb33b0c3c7 | ||
|
|
93471f4342 | ||
|
|
0db0bfdfd4 | ||
|
|
92e4dbcaa8 | ||
|
|
15443d155b | ||
|
|
21283962c4 | ||
|
|
1816aa2761 | ||
|
|
3d862b03eb | ||
|
|
411fca2ab3 | ||
|
|
8190907e39 | ||
|
|
05fbc0b80d | ||
|
|
347397f4da | ||
|
|
1d0e37950d | ||
|
|
7828df20fc | ||
|
|
a72f6b964f | ||
|
|
950dc05843 | ||
|
|
6a242635f7 | ||
|
|
dff1bd0d40 | ||
|
|
d678390b17 | ||
|
|
579b0bc5ef | ||
|
|
23b01416d7 | ||
|
|
3266b33b05 | ||
|
|
ad686553e5 | ||
|
|
e289f5acbd | ||
|
|
1573f73a0e | ||
|
|
efcac585ef | ||
|
|
89757bc7dd | ||
|
|
04e8391ca2 | ||
|
|
7f6bda6c76 | ||
|
|
b3b5063b50 | ||
|
|
a79d9aa094 | ||
|
|
b8f3200f8f | ||
|
|
b702566cb0 | ||
|
|
69b4d0a322 | ||
|
|
b2d17e3639 | ||
|
|
29ef585c31 | ||
|
|
d3d1c4ccb9 | ||
|
|
f53a260dff | ||
|
|
e8e86d1bf4 | ||
|
|
284d8f07fa | ||
|
|
aca1d26a76 | ||
|
|
3ef5c28bac | ||
|
|
de5c9f6047 | ||
|
|
b4b93af96a | ||
|
|
9d52e8853e | ||
|
|
2369c9b3be | ||
|
|
8ee0699b3e | ||
|
|
fcb18e56f0 | ||
|
|
14bbab0969 | ||
|
|
d84fe65460 | ||
|
|
047b8d1433 | ||
|
|
6e699a27ab | ||
|
|
fd45d20876 | ||
|
|
f977ff30ea | ||
|
|
27946aabae | ||
|
|
acdc935f70 | ||
|
|
28cd928a1b | ||
|
|
405f09ff00 | ||
|
|
9c1ac14bf1 | ||
|
|
920afd6045 | ||
|
|
60912da17c | ||
|
|
68eff16f64 | ||
|
|
475eb0565d | ||
|
|
cc58b6739b | ||
|
|
3f66a76727 | ||
|
|
deaec751b0 | ||
|
|
baff0ab7e4 | ||
|
|
45a56f6ca3 | ||
|
|
8a14756cea | ||
|
|
c39b0f1e02 | ||
|
|
27d8706023 | ||
|
|
ef434e5491 | ||
|
|
88c960dfad | ||
|
|
d028dee724 | ||
|
|
d17a9e2ef1 | ||
|
|
85399792fb | ||
|
|
a02ed17233 | ||
|
|
3dc5f1c843 | ||
|
|
2a077ecf81 | ||
|
|
1ae5fe8cc3 | ||
|
|
658ac6b759 | ||
|
|
08973f9b50 | ||
|
|
1abd3e2aa7 | ||
|
|
49929dd4a0 | ||
|
|
15cce31886 | ||
|
|
0302556635 | ||
|
|
1ae8ff4b99 | ||
|
|
ca75a702ca | ||
|
|
14096aeaf8 | ||
|
|
b9ebef7817 | ||
|
|
b4f6f8f2d0 | ||
|
|
d27b2f9727 | ||
|
|
fd0e58e957 | ||
|
|
eaf08c60d4 | ||
|
|
6e900d111c | ||
|
|
cf6a192ccc | ||
|
|
515a8d2b54 | ||
|
|
4018604028 | ||
|
|
55d70f8454 | ||
|
|
fd08b5d4ee | ||
|
|
f7cd18d4ad | ||
|
|
078e098b28 | ||
|
|
b2f761ef76 | ||
|
|
cdd894b50c | ||
|
|
323524ac1b | ||
|
|
bc04ca3db7 | ||
|
|
434d2c76e0 | ||
|
|
009aecc72a | ||
|
|
ddd7885038 | ||
|
|
a46a7f5429 | ||
|
|
fe5e7011cc | ||
|
|
e12da9e82d | ||
|
|
dddf4d12df | ||
|
|
96033cbc78 | ||
|
|
b821d3fd09 | ||
|
|
1b6dcb00e7 | ||
|
|
938e4e1ad7 | ||
|
|
cd99b04507 | ||
|
|
ba49cde9f6 | ||
|
|
3f82e25074 | ||
|
|
f132f88059 | ||
|
|
38ff9e748a | ||
|
|
c299e0c3e2 | ||
|
|
d4127889c3 | ||
|
|
994ead0e3e | ||
|
|
3e5900b1e0 | ||
|
|
f579c43b29 | ||
|
|
f2fee6779d | ||
|
|
4abb7ecc46 | ||
|
|
7e8fa56bf5 | ||
|
|
ce02ba86db | ||
|
|
7fcb585268 | ||
|
|
e143280c6c | ||
|
|
f82d36afb0 | ||
|
|
e14d899ac4 | ||
|
|
6a16cbabd2 | ||
|
|
e52711d592 | ||
|
|
abc01538f9 | ||
|
|
6eaf4359b5 | ||
|
|
728959332b | ||
|
|
d58a0981c4 | ||
|
|
23cdcffb12 | ||
|
|
944363765a | ||
|
|
ace3bdbb8f | ||
|
|
7926b9fd0f | ||
|
|
4617b34478 | ||
|
|
7fcda1f0e7 | ||
|
|
a72842cafe | ||
|
|
8ffbfd5653 | ||
|
|
955681886f | ||
|
|
066656adf8 | ||
|
|
a5db2a49a8 | ||
|
|
4a5dd111d7 | ||
|
|
73d260c2db | ||
|
|
c86e895ea9 | ||
|
|
208bdb28fe | ||
|
|
70b3b2f75b | ||
|
|
8f35603493 | ||
|
|
1c592e6713 | ||
|
|
b0d22fc035 | ||
|
|
7f5c9dd4ef | ||
|
|
76e2a3ef46 | ||
|
|
9c48b0054f | ||
|
|
72db97df79 | ||
|
|
a8b8b49dce | ||
|
|
26f9939bff | ||
|
|
37ec94c157 | ||
|
|
b336b2e54e | ||
|
|
91a0cecffa | ||
|
|
a59204cd99 | ||
|
|
cd61c3fd8d | ||
|
|
34082cb122 | ||
|
|
70d92cef40 | ||
|
|
b3c1a97f3d | ||
|
|
f3aa63aa13 | ||
|
|
27c8753b3a | ||
|
|
d352007cd2 | ||
|
|
3fe95f4362 | ||
|
|
f13a201c56 | ||
|
|
d4ca8c77b5 | ||
|
|
920690e504 | ||
|
|
282eebce76 | ||
|
|
abeddf8c65 | ||
|
|
826cafc3a9 | ||
|
|
04422376f2 | ||
|
|
f88f805f28 | ||
|
|
bfe4ec2818 | ||
|
|
9e07d636a8 | ||
|
|
b10211dcf9 | ||
|
|
9a7c88efbf | ||
|
|
a0750dddfc | ||
|
|
90b0100048 | ||
|
|
bbb5763b8c | ||
|
|
b029070d94 | ||
|
|
499b2dc748 | ||
|
|
153f1cbd31 | ||
|
|
9247f96679 | ||
|
|
d18d138f73 | ||
|
|
36f0686cf4 | ||
|
|
963e637931 | ||
|
|
570d5bc60b | ||
|
|
4c37af4976 | ||
|
|
f09fbdba72 | ||
|
|
17cd4b0460 | ||
|
|
02e234a627 | ||
|
|
79c13c7f31 | ||
|
|
c86ae98d04 | ||
|
|
aec129f6bb | ||
|
|
b7d6e06530 | ||
|
|
d1fc654e36 | ||
|
|
1e99dbe589 | ||
|
|
80a5f6188b | ||
|
|
265728dc38 | ||
|
|
863a29dbe6 | ||
|
|
b28330ed56 | ||
|
|
4ec7666985 | ||
|
|
63cf643300 | ||
|
|
85c167a5f4 | ||
|
|
1f4fbbd5f8 | ||
|
|
d9405428d6 | ||
|
|
91280c2177 | ||
|
|
9abba00fe1 | ||
|
|
cd2b36e5c7 | ||
|
|
8552d7e825 | ||
|
|
5e1e9399cc | ||
|
|
6a5e4af0f3 | ||
|
|
38b31fce3c | ||
|
|
c5fa140d07 | ||
|
|
e13f1cf73a | ||
|
|
03ff3f7257 | ||
|
|
4c990f43a7 | ||
|
|
75497aa031 | ||
|
|
a8c69d1cec | ||
|
|
dc4c1b9b2f | ||
|
|
1ef654578e | ||
|
|
a610fe2fd2 | ||
|
|
97cbb8a03f | ||
|
|
0e28127851 | ||
|
|
c7c41805d8 | ||
|
|
6cef45d3bd | ||
|
|
ded05a5408 | ||
|
|
361a4cc242 | ||
|
|
6cfed6c229 | ||
|
|
e8d62ab190 | ||
|
|
7a027c4149 | ||
|
|
425de91c57 | ||
|
|
d50aef3832 | ||
|
|
afe2b1b8ce | ||
|
|
566fff6c0c | ||
|
|
88b9c926c4 | ||
|
|
2db13a7ec1 | ||
|
|
4d6b740f30 | ||
|
|
9a8025777b | ||
|
|
715bc2ecbd | ||
|
|
e05c456f99 | ||
|
|
3c4459bbac | ||
|
|
cd128c5683 | ||
|
|
b4cfec133d | ||
|
|
408c5351bf | ||
|
|
6fd295211b | ||
|
|
6e9db6d7c9 | ||
|
|
cd4964401f | ||
|
|
7572febd8d | ||
|
|
90180b890d | ||
|
|
1e6b0ac6a9 | ||
|
|
6ab9b5fba7 | ||
|
|
b9cbf749be | ||
|
|
ea7de1c085 | ||
|
|
eaa681dfc3 | ||
|
|
2d79c3df9f | ||
|
|
649134bd22 | ||
|
|
abf3192476 | ||
|
|
b7a2312994 | ||
|
|
4d6d16a58b | ||
|
|
4d389dcfa3 | ||
|
|
01fa48df18 | ||
|
|
3fedf57b72 | ||
|
|
3140c2e90c | ||
|
|
5fbab18684 | ||
|
|
1fc7c4d53d | ||
|
|
2f14216e49 | ||
|
|
ae602a2f6f | ||
|
|
08c701a3eb | ||
|
|
40cebf61c2 | ||
|
|
c6fa18fe8d | ||
|
|
3598d5efdf | ||
|
|
0b305ec22f | ||
|
|
fe694de119 | ||
|
|
74bb03f92e | ||
|
|
3446abd7d8 | ||
|
|
ed0cc6c9be | ||
|
|
ef8d6ddf07 | ||
|
|
3a3dd05393 | ||
|
|
f6417a3bd5 | ||
|
|
49df4425ea | ||
|
|
758129d035 | ||
|
|
608e6e7a1d | ||
|
|
6cffdba578 | ||
|
|
9b64cc270a | ||
|
|
cc5526f7f3 | ||
|
|
77ed94a2e8 | ||
|
|
54657e5dc7 | ||
|
|
62bc3dd614 | ||
|
|
ec7f10d119 | ||
|
|
50f22ccf25 | ||
|
|
9ae4a5053a | ||
|
|
807bddeace | ||
|
|
74ce6ca353 | ||
|
|
cb10380498 | ||
|
|
490f850834 | ||
|
|
9bd75acdba | ||
|
|
e8e8bd3262 | ||
|
|
55411f7979 | ||
|
|
40e8d39f75 | ||
|
|
bd81680cdb | ||
|
|
fa557969df | ||
|
|
2a5f92eaf2 | ||
|
|
7cd5dd9929 | ||
|
|
a6f18efbc7 | ||
|
|
fedaa33741 | ||
|
|
1eadd06f9d | ||
|
|
4f1c2731bf | ||
|
|
00b52794aa | ||
|
|
f9c2799526 | ||
|
|
bd1278aab4 | ||
|
|
4f8063214f | ||
|
|
fcdf2fba0e | ||
|
|
a8479fb315 | ||
|
|
9fc473efb1 | ||
|
|
712785dc44 | ||
|
|
f904057944 | ||
|
|
4cc750ec0f | ||
|
|
f97f43f49b | ||
|
|
826e42085c | ||
|
|
4e555f3198 | ||
|
|
df5d23abbc | ||
|
|
8041f6b268 | ||
|
|
f22e2d6d93 | ||
|
|
d770b218f2 | ||
|
|
93d640206f | ||
|
|
19d66a3cb2 | ||
|
|
9819668246 | ||
|
|
944a29814b | ||
|
|
b09ef174ab | ||
|
|
aebf33c2f0 | ||
|
|
5e59b7b561 | ||
|
|
f47efb8f5f | ||
|
|
c562b9525a | ||
|
|
06593afb95 | ||
|
|
9d09869f80 | ||
|
|
32ecbfc718 | ||
|
|
ae6fdeef29 | ||
|
|
bbd0cdee78 | ||
|
|
76ab984e0b | ||
|
|
c61c9c86c8 | ||
|
|
90e29f8b43 | ||
|
|
e7ab1e4296 | ||
|
|
e5017a2031 | ||
|
|
02ff35a306 | ||
|
|
c3b90d7952 | ||
|
|
f9ba58c6f1 | ||
|
|
69da2dbd18 | ||
|
|
308b4a9ecd | ||
|
|
1256f02c13 | ||
|
|
b3ee050eb3 | ||
|
|
ead26a92c4 | ||
|
|
2e4dca4d89 | ||
|
|
b4d1e3eaff | ||
|
|
8a5fa4f158 | ||
|
|
fcacca3495 | ||
|
|
6e71478598 | ||
|
|
e982a8a799 | ||
|
|
b591947fac | ||
|
|
6ed171fc8f | ||
|
|
48f68f82dd | ||
|
|
70fbc0c24a | ||
|
|
27049809ef | ||
|
|
5b4ff8b5c2 | ||
|
|
0f581ad9cb | ||
|
|
edf9163ca1 | ||
|
|
83b6e82c1d | ||
|
|
c0e54d44c4 | ||
|
|
03f20e7ec7 | ||
|
|
c1e9d01099 | ||
|
|
0f4f2674f7 | ||
|
|
6e752bba1c | ||
|
|
077d6ba8a4 | ||
|
|
0e3b9d9c1d | ||
|
|
21edb60c16 | ||
|
|
b38cd59b42 | ||
|
|
fbf88be324 | ||
|
|
cfa228083d | ||
|
|
b9b36e31d2 | ||
|
|
f25259e78b | ||
|
|
7e24125fe3 | ||
|
|
e237f76b0f | ||
|
|
a5c9a8cea6 | ||
|
|
7dca17f2aa | ||
|
|
2befc5c172 | ||
|
|
8a667cb8a2 | ||
|
|
fde1e96104 | ||
|
|
13ad09022b | ||
|
|
7c248803dd | ||
|
|
b4b440f871 | ||
|
|
86c29a89c2 | ||
|
|
ac3255efb5 | ||
|
|
24acd8e2fe | ||
|
|
a243d7a0ff | ||
|
|
530715af17 | ||
|
|
10e2c0c20f | ||
|
|
17b1de0825 | ||
|
|
882478edb1 | ||
|
|
a5e3343f06 | ||
|
|
168e4427dc | ||
|
|
069a44774e | ||
|
|
e67d60d088 | ||
|
|
d1239e2e22 | ||
|
|
3e1ee77c14 | ||
|
|
973f1386d0 | ||
|
|
077ed36a4a | ||
|
|
1841ee32f6 | ||
|
|
d16a1938c2 | ||
|
|
28f3c3be62 | ||
|
|
1d2e0fed0e | ||
|
|
065048ee46 | ||
|
|
465a982080 | ||
|
|
8827fae5f3 | ||
|
|
3461a2d36c | ||
|
|
19891a39e4 | ||
|
|
b751959f42 | ||
|
|
721a599ed3 | ||
|
|
9caf10774b | ||
|
|
b1dd37025f | ||
|
|
9b49a321bb | ||
|
|
f8b0214519 | ||
|
|
4bcc9dbc3a | ||
|
|
4b44f3c718 | ||
|
|
230ce5e405 | ||
|
|
ca2bc58881 | ||
|
|
8271ccb005 | ||
|
|
e766a9a581 | ||
|
|
b66a0a9b12 | ||
|
|
fb7733f2e1 | ||
|
|
d4fe8943a4 | ||
|
|
c3d6e845d0 | ||
|
|
ba7b0843df | ||
|
|
5bbd6d474a | ||
|
|
e3d1d2760a | ||
|
|
4489e171db | ||
|
|
9c32bf2328 | ||
|
|
1c687f84b7 | ||
|
|
a574687253 | ||
|
|
b7464a3742 | ||
|
|
f319d2f498 | ||
|
|
61bb42ae3a | ||
|
|
e5bd0dfe49 | ||
|
|
f30adeb613 | ||
|
|
ca173694f8 | ||
|
|
9b04008545 | ||
|
|
83ca847312 | ||
|
|
19dcb55980 | ||
|
|
f1f1e19670 | ||
|
|
1bb23e2198 | ||
|
|
adfd7baa46 | ||
|
|
c0b33e1ea5 | ||
|
|
066bb84e5c | ||
|
|
e230ee77fd | ||
|
|
e113373eb6 | ||
|
|
dd602cd3b6 | ||
|
|
ce029693cf | ||
|
|
497af1fe00 | ||
|
|
b5e5bc0b01 | ||
|
|
051b1875e7 | ||
|
|
a673f4a7f2 | ||
|
|
bd1804589a | ||
|
|
26409139a0 | ||
|
|
62e44488a9 | ||
|
|
5def82024d | ||
|
|
cdb6b6d8f8 | ||
|
|
4705c3ec8c | ||
|
|
ded547a296 | ||
|
|
c61655bb08 | ||
|
|
eb25eeef76 | ||
|
|
1de015a3ab | ||
|
|
b83dbbe5d4 | ||
|
|
ed82534030 | ||
|
|
94481421ac | ||
|
|
3463563e42 | ||
|
|
b6fac871e0 | ||
|
|
6221193d8c | ||
|
|
83f14010b1 | ||
|
|
adb6f8327d | ||
|
|
bb7ede7998 | ||
|
|
b6103a61cd | ||
|
|
8ac4b2c7dd | ||
|
|
ed15907e69 | ||
|
|
022f358e16 | ||
|
|
661ce426dd | ||
|
|
068b89fe72 | ||
|
|
0924ebe33d | ||
|
|
97b7429da5 | ||
|
|
cc1c392418 | ||
|
|
1e8ce0bebf | ||
|
|
badcdf78c1 | ||
|
|
026bf60751 | ||
|
|
6aee85a591 | ||
|
|
f8e150c65d | ||
|
|
694d0ac27c | ||
|
|
2ad683f7dd | ||
|
|
da367ee93f | ||
|
|
190d2304bc | ||
|
|
bdc39b25b7 | ||
|
|
9b17434c64 | ||
|
|
65426df448 | ||
|
|
05977c7792 | ||
|
|
804e92e9b7 | ||
|
|
02420b5e94 | ||
|
|
b3f7016515 | ||
|
|
96c7166a26 | ||
|
|
4262a1b639 | ||
|
|
ad9316377d | ||
|
|
c282f9c2d4 | ||
|
|
52a8ce60d9 | ||
|
|
517a87438e | ||
|
|
cf4d03b772 | ||
|
|
8877be24b5 | ||
|
|
1bbe9e19de | ||
|
|
6728f7c796 | ||
|
|
5488f4ba3d | ||
|
|
026b4aa7cf | ||
|
|
ab21227bee | ||
|
|
de0afbabde | ||
|
|
ef033e477c | ||
|
|
35b2cbc490 | ||
|
|
ecc031422e | ||
|
|
7451223e09 | ||
|
|
9a1ae415ce | ||
|
|
e09fe00563 | ||
|
|
0d8ae55422 | ||
|
|
ee508a7927 | ||
|
|
ec8b282767 | ||
|
|
face17249d | ||
|
|
ce182c9fa5 | ||
|
|
b25a59e748 | ||
|
|
3b0c50329a | ||
|
|
420245e66a | ||
|
|
dd9db08431 | ||
|
|
f8122d5fb3 | ||
|
|
90975c31d2 | ||
|
|
8dd630b43b | ||
|
|
25a41858fd | ||
|
|
d20986949b | ||
|
|
bc60281379 | ||
|
|
84b3df80e2 | ||
|
|
8923315995 | ||
|
|
528b4584ac | ||
|
|
c14d2c3867 | ||
|
|
be3081de42 | ||
|
|
c46e0d9811 | ||
|
|
f00df1c17a | ||
|
|
684f8769a1 | ||
|
|
956888956e | ||
|
|
d8de1f86cf | ||
|
|
4be4b84622 | ||
|
|
ce1fbb5e05 | ||
|
|
c29a7cd693 | ||
|
|
b69361abf2 | ||
|
|
e7e50bee35 | ||
|
|
2cef319a32 | ||
|
|
9df7c18181 | ||
|
|
b4229070e6 | ||
|
|
c11632f8ef | ||
|
|
972cd5e645 | ||
|
|
87e606699d | ||
|
|
5da4dee1cb | ||
|
|
0d44809496 | ||
|
|
74e0108d76 | ||
|
|
243fe9c756 | ||
|
|
276dff3ae1 | ||
|
|
e1ed469ec9 | ||
|
|
d60ac945fc | ||
|
|
82d0f81498 | ||
|
|
f176c1f117 | ||
|
|
341f4c006f | ||
|
|
a86721f992 | ||
|
|
d50805da83 | ||
|
|
7532cec613 | ||
|
|
6ef4e44c88 | ||
|
|
c147acedda | ||
|
|
b60c4e24e6 | ||
|
|
b4d8c82e04 | ||
|
|
4c197400da | ||
|
|
b422c44603 | ||
|
|
8fd7a5ad8f | ||
|
|
5cfcf77c32 | ||
|
|
e15a40f7d9 | ||
|
|
62905aa073 | ||
|
|
b98d3f7be2 | ||
|
|
1808a0473c | ||
|
|
1d8d609307 | ||
|
|
c8f9645eae | ||
|
|
0b83f75e14 | ||
|
|
ee2199afce | ||
|
|
b7a3719815 | ||
|
|
2b7a5c31d4 | ||
|
|
015c72292f | ||
|
|
303619d060 | ||
|
|
219c8c0ccc | ||
|
|
d3158cc8a1 | ||
|
|
d4c69004cc | ||
|
|
c0cd027904 | ||
|
|
12675a1586 | ||
|
|
a51d89f559 | ||
|
|
2c4d41f8da | ||
|
|
6491fc3ddd | ||
|
|
a51189e6f6 | ||
|
|
3bdee559e6 | ||
|
|
de67e11704 | ||
|
|
180380339d | ||
|
|
1d7be34787 | ||
|
|
d8e3c1a390 | ||
|
|
c4bf136fbd | ||
|
|
dc567ea94f | ||
|
|
b4133dbb7e | ||
|
|
3c8f2855bf | ||
|
|
9583a2a9e9 | ||
|
|
37646b4f63 | ||
|
|
96adf59413 | ||
|
|
6fadc6f4f8 | ||
|
|
4a2be8898d | ||
|
|
8bfbc93cf8 | ||
|
|
cb139b11ca | ||
|
|
e1e3ccc599 | ||
|
|
00e7d0793a | ||
|
|
6a0e2528db | ||
|
|
c612810a2a | ||
|
|
21243fd7a0 | ||
|
|
ca1b010529 | ||
|
|
a2d7297382 | ||
|
|
6052103ebf | ||
|
|
383cbe953f | ||
|
|
75d20f7e0f | ||
|
|
9fe7644f0a | ||
|
|
863c41d550 | ||
|
|
5280f71e15 | ||
|
|
df7b0772c8 | ||
|
|
4ea63506a5 | ||
|
|
fae1253df6 | ||
|
|
d9e074762b | ||
|
|
096cd4f9b9 | ||
|
|
39f6c8959e | ||
|
|
3824bcb359 | ||
|
|
3b56ce55ed | ||
|
|
31b8c24b4b | ||
|
|
a0b97b51c9 | ||
|
|
f45925d34d | ||
|
|
90a34c1295 | ||
|
|
42717759b6 | ||
|
|
695ca73817 | ||
|
|
f21a95940e | ||
|
|
7fe21927c9 | ||
|
|
8338777318 | ||
|
|
e2c1b65e9e | ||
|
|
e4f397d593 | ||
|
|
c0893aa500 | ||
|
|
726d78d4f8 | ||
|
|
918e31483b | ||
|
|
54960a9a99 | ||
|
|
71feae1ee1 | ||
|
|
317bf35c9c | ||
|
|
73b8fa039f | ||
|
|
8e1412c014 | ||
|
|
8cc777013c | ||
|
|
750f98648d | ||
|
|
995f36075a |
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = false
|
||||
@@ -9,9 +9,12 @@
|
||||
# Application URL
|
||||
APP_URL=https://example.com
|
||||
|
||||
# Use HTTP protocol and redirect to HTTPS
|
||||
# Use HTTPS 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
|
||||
|
||||
# Timezones: https://github.com/freescout-helpdesk/freescout/wiki/PHP-Timezones
|
||||
# Comment it to use default timezone from php.ini
|
||||
#APP_TIMEZONE=Europe/London
|
||||
@@ -29,4 +32,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
|
||||
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: FreeScout
|
||||
13
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: General Help Request
|
||||
about: Create a general help request
|
||||
|
||||
---
|
||||
|
||||
* 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
|
||||
* 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
|
||||
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
|
||||
|
||||
Still here? Well clean this out and go ahead :)
|
||||
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...
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,6 +27,7 @@ Thumbs.db
|
||||
/Modules
|
||||
/Modules/**/.git
|
||||
/public/modules
|
||||
/public/docs
|
||||
/storage/.ignore_locales
|
||||
/storage/.installed
|
||||
/tools
|
||||
|
||||
@@ -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
|
||||
|
||||
56
README.md
56
README.md
@@ -5,12 +5,11 @@
|
||||
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||
<br/><br/>
|
||||
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://github.com/freescout-helpdesk/freescout#requirements)
|
||||
[](https://freescout-helpdesk.github.io/img/badges/travis.svg) [](http://hits.dwyl.io/freescout-helpdesk/freescout)
|
||||
[](https://github.com/freescout-helpdesk/freescout#requirements) [](https://hits.seeyoufarm.com)
|
||||
|
||||
</div>
|
||||
|
||||
**FreeScout** is the super lightweight free open source help desk and shared inbox written in PHP7 (Laravel 5.5 framework) – self hosted clone of HelpScout. Now you can enjoy free Zendesk & Help Scout without giving up privacy or locking yourself into a service you don't control. FreeScout has been developed from scratch and is not using any copyrighted Help Scout or Zendesk materials.
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
@@ -18,29 +17,34 @@
|
||||
* [Features](#features)
|
||||
* [Demo](#demo)
|
||||
* [Mobile Apps](#mobile-apps)
|
||||
* [Modules](#modules)
|
||||
* [Requirements](#requirements)
|
||||
* [News & Updates](#news--updates)
|
||||
* [Installation](#installation)
|
||||
* [Cloud Hosted](#cloud-hosted)
|
||||
* [Modules](#modules)
|
||||
* [Tools & Integrations](#tools--integrations)
|
||||
* [News & Updates](#news--updates)
|
||||
* [Contributing](#contributing)
|
||||
* [Screenshots](#screenshots)
|
||||
|
||||
## 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, Italian, Portuguese, Russian, Dutch).
|
||||
* Multilingual (English, Croatian, Czech, Danish, Dutch, French, German, Italian, Japanese, Korean, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish).
|
||||
* Seamless email integration.
|
||||
* Suppports modern Microsoft Exchange authentication.
|
||||
* Web installer & updater.
|
||||
* Starred conversations.
|
||||
* Forwarding conversations.
|
||||
* Merging conversations.
|
||||
* Moving conversations between mailboxes.
|
||||
* Phone conversations.
|
||||
* Sending new conversations to multiple recipients at once.
|
||||
* Collision detection – notice is shown when two agents open the same conversation.
|
||||
* Push notifications.
|
||||
* Following a conversation.
|
||||
* Auto reply.
|
||||
* Notes.
|
||||
* Internal notes.
|
||||
* Pasting screenshots from the clipboard into the reply area.
|
||||
* Configuring notifications on a per user basis.
|
||||
* Open tracking.
|
||||
@@ -55,24 +59,18 @@ Need anything else? Suggest features [here](https://freescout.net/request-featur
|
||||
|
||||
## Mobile Apps
|
||||
|
||||
<a href="https://freescout.net/android-app/" target="_blank"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a>
|
||||
Mobile apps support the same functionality and modules as the web version of your FreeScout installation. Both support agents and administrators can use mobile apps.
|
||||
|
||||
## Modules
|
||||
|
||||
[Available modules](https://freescout.net/modules/)
|
||||
<a href="https://freescout.net/android-app/" target="_blank" rel="nofollow"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a> <a href="https://freescout.net/ios-app/" target="_blank" rel="nofollow"><img alt="iOS App" src="https://freescout-helpdesk.github.io/img/apps/ios.png?v=1" width="200px" /></a>
|
||||
|
||||
## Requirements
|
||||
|
||||
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
|
||||
|
||||
* Nginx / Apache / IIS
|
||||
* PHP 7.0 - 7.3
|
||||
* PHP 7.1 - 8.1
|
||||
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
|
||||
|
||||
## News & Updates
|
||||
|
||||
If you would like to be notified about new releases, news and updates you can subscribe [here](https://feedburner.google.com/fb/a/mailverify?uri=freescout)
|
||||
|
||||
## Installation
|
||||
|
||||
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
|
||||
@@ -82,13 +80,35 @@ Images & one-click installs:
|
||||
* [Docker Image](http://freescout.net/docker/)
|
||||
* [Softaculous](http://www.softaculous.com/apps/customersupport/FreeScout) (cPanel, Plesk, ISPmanager, H-Sphere, DirectAdmin, InterWorx)
|
||||
* [Fantastico](http://ff3.netenberg.com/visitors/scripts/freescout/view) (cPanel, DirectAdmin, ISP Manager, ISP Config)
|
||||
* [Cloudron](https://cloudron.io/store/net.freescout.cloudronapp.html)
|
||||
* [Ubuntu](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
|
||||
|
||||
## Cloud Hosted
|
||||
|
||||
[Cloud Hosted FreeScout](https://freescout.net/cloud-hosted/)
|
||||
|
||||
## Modules
|
||||
|
||||
* [Official Modules](https://freescout.net/modules/)
|
||||
* [Community Modules](https://freescout.net/community-modules/)
|
||||
|
||||
## Tools & Integrations
|
||||
|
||||
* [API](https://api-docs.freescout.net/)
|
||||
* [Migrate to FreeScout](http://freescout.net/migrate/) (from any help desk)
|
||||
* [Zapier](https://freescout.net/zapier/)
|
||||
* [Integromat](https://freescout.net/integromat/)
|
||||
|
||||
## News & Updates
|
||||
|
||||
If you would like to be notified about important news and updates you can subscribe [here](https://freescout.net/subscribe/)
|
||||
|
||||
## Contributing
|
||||
|
||||
* [Support the project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/issues/288)
|
||||
* [Development Guide](https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide)
|
||||
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
|
||||
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
|
||||
* [Support project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/wiki#support-project)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Email at support@freescout.net
|
||||
@@ -96,7 +96,7 @@ class ActivityLog extends Activity
|
||||
case self::NAME_APP_LOGS:
|
||||
return __('App Logs');
|
||||
default:
|
||||
return ucfirst($log_name);
|
||||
return ucwords(str_replace('_', ' ', $log_name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ class Attachment extends Model
|
||||
|
||||
const DIRECTORY = 'attachment';
|
||||
|
||||
CONST DISK = 'private';
|
||||
|
||||
// https://github.com/Webklex/laravel-imap/blob/master/src/IMAP/Attachment.php
|
||||
public static $types = [
|
||||
'message' => self::TYPE_MESSAGE,
|
||||
@@ -32,13 +34,8 @@ class Attachment extends Model
|
||||
'other' => self::TYPE_OTHER,
|
||||
];
|
||||
|
||||
/**
|
||||
* Files with such extensions are being renamed on upload.
|
||||
*/
|
||||
public static $restricted_extensions = [
|
||||
'php.*',
|
||||
'sh',
|
||||
'pl',
|
||||
public static $type_extensions = [
|
||||
self::TYPE_VIDEO => ['flv', 'mp4', 'm3u8', 'ts', '3gp', 'mov', 'avi', 'wmv']
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
@@ -60,19 +57,38 @@ class Attachment extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check extension.
|
||||
$extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', $extension)) {
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = $file_name.'_';
|
||||
}
|
||||
$orig_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = \Helper::sanitizeUploadedFileName($file_name);
|
||||
|
||||
// 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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen($file_name) > 255) {
|
||||
$without_ext = pathinfo($file_name, PATHINFO_FILENAME);
|
||||
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
// 125 because file name may have unicode symbols.
|
||||
$file_name = \Helper::substrUnicode($without_ext, 0, 125-strlen($extension)-1);
|
||||
$file_name .= '.'.$extension;
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
$type = self::detectType($mime_type);
|
||||
$type = self::detectType($mime_type, $orig_extension);
|
||||
}
|
||||
|
||||
$attachment = new self();
|
||||
@@ -84,6 +100,20 @@ class Attachment extends Model
|
||||
$attachment->embedded = $embedded;
|
||||
$attachment->save();
|
||||
|
||||
$file_info = self::saveFileToDisk($attachment, $file_name, $content, $uploaded_file);
|
||||
|
||||
$attachment->file_dir = $file_info['file_dir'];
|
||||
$attachment->size = Storage::disk(self::DISK)->size($file_info['file_path']);
|
||||
$attachment->save();
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file to the disk and return file_dir.
|
||||
*/
|
||||
public static function saveFileToDisk($attachment, $file_name, $content, $uploaded_file)
|
||||
{
|
||||
// Save file from content or copy file.
|
||||
// We have to keep file name as is, so if file exists we create extra folder.
|
||||
// Examples: 1/2/3
|
||||
@@ -93,21 +123,20 @@ class Attachment extends Model
|
||||
do {
|
||||
$i++;
|
||||
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
|
||||
} while (Storage::exists($file_path));
|
||||
} while (Storage::disk(self::DISK)->exists($file_path));
|
||||
|
||||
$file_dir .= $i.DIRECTORY_SEPARATOR;
|
||||
|
||||
if ($uploaded_file) {
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name);
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
|
||||
} else {
|
||||
Storage::put($file_path, $content);
|
||||
Storage::disk(self::DISK)->put($file_path, $content);
|
||||
}
|
||||
|
||||
$attachment->file_dir = $file_dir;
|
||||
$attachment->size = Storage::size($file_path);
|
||||
$attachment->save();
|
||||
|
||||
return $attachment;
|
||||
return [
|
||||
'file_dir' => $file_dir,
|
||||
'file_path' => $file_path,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,13 +178,18 @@ class Attachment extends Model
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function detectType($mime_type)
|
||||
public static function detectType($mime_type, $extension = '')
|
||||
{
|
||||
if (preg_match("/^text\//", $mime_type)) {
|
||||
return self::TYPE_TEXT;
|
||||
} elseif (preg_match("/^message\//", $mime_type)) {
|
||||
return self::TYPE_MESSAGE;
|
||||
} elseif (preg_match("/^application\//", $mime_type)) {
|
||||
// This is tricky mime type.
|
||||
// For .mp4 mime type can be application/octet-stream
|
||||
if (!empty($extension) && in_array(strtolower($extension), self::$type_extensions[self::TYPE_VIDEO])) {
|
||||
return self::TYPE_VIDEO;
|
||||
}
|
||||
return self::TYPE_APPLICATION;
|
||||
} elseif (preg_match("/^audio\//", $mime_type)) {
|
||||
return self::TYPE_AUDIO;
|
||||
@@ -189,7 +223,40 @@ class Attachment extends Model
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return Storage::url($this->getStorageFilePath());
|
||||
return Storage::url($this->getStorageFilePath()).'?id='.$this->id.'&token='.$this->getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashed security token for the attachment.
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
// \Hash::make() may contain . and / symbols which may cause problems.
|
||||
return md5(config('app.key').$this->id.$this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the current Attachment as download
|
||||
*/
|
||||
public function download($view = false)
|
||||
{
|
||||
$headers = [];
|
||||
// #533
|
||||
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
|
||||
if ($view) {
|
||||
$headers['Content-Disposition'] = '';
|
||||
}
|
||||
$file_name = $this->file_name;
|
||||
|
||||
if ($file_name == "RFC822"){
|
||||
$file_name = $file_name.'.eml';
|
||||
}
|
||||
|
||||
return $this->getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
|
||||
}
|
||||
|
||||
private function getDisk() {
|
||||
return Storage::disk(self::DISK);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,14 +269,24 @@ class Attachment extends Model
|
||||
return self::formatBytes($this->size);
|
||||
}
|
||||
|
||||
/**
|
||||
* attachment/...
|
||||
*/
|
||||
public function getStorageFilePath()
|
||||
{
|
||||
return self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||
}
|
||||
|
||||
public function getLocalFilePath()
|
||||
/**
|
||||
* /var/html/storage/app/attachment/...
|
||||
*/
|
||||
public function getLocalFilePath($full = true)
|
||||
{
|
||||
return Storage::path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
if ($full) {
|
||||
return $this->getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
} else {
|
||||
return DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||
}
|
||||
}
|
||||
|
||||
public static function formatBytes($size, $precision = 0)
|
||||
@@ -260,7 +337,7 @@ class Attachment extends Model
|
||||
{
|
||||
// Delete from disk
|
||||
foreach ($attachments as $attachment) {
|
||||
Storage::delete($attachment->getStorageFilePath());
|
||||
$attachment->getDisk()->delete($attachment->getStorageFilePath());
|
||||
}
|
||||
|
||||
// Delete from DB
|
||||
@@ -268,21 +345,65 @@ class Attachment extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dummy ID for attachment.
|
||||
* Not used for now.
|
||||
* Delete attachments and update Thread & Conversation.
|
||||
*/
|
||||
/*public static function genrateDummyId()
|
||||
public static function deleteAttachments($attachments)
|
||||
{
|
||||
if (!$this->id) {
|
||||
// Math.random should be unique because of its seeding algorithm.
|
||||
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
|
||||
// after the decimal.
|
||||
$hash = mt_rand() / mt_getrandmax();
|
||||
$hash = base_convert($hash, 10, 36);
|
||||
$id = substr($hash, 2, 9);
|
||||
} else {
|
||||
$id = substr(md5($this->id), 0, 9);
|
||||
if (!$attachments instanceof \Illuminate\Support\Collection) {
|
||||
$attachments = collect($attachments);
|
||||
}
|
||||
return $id;
|
||||
}*/
|
||||
|
||||
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)
|
||||
{
|
||||
$new_attachment = $this->replicate();
|
||||
$new_attachment->thread_id = $thread_id;
|
||||
|
||||
$new_attachment->save();
|
||||
|
||||
try {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$this->getLocalFilePath(), $this->file_name,
|
||||
null, null, true
|
||||
);
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
|
||||
|
||||
if (!empty($file_info['file_dir'])) {
|
||||
$new_attachment->file_dir = $file_info['file_dir'];
|
||||
$new_attachment->save();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e);
|
||||
}
|
||||
|
||||
return $new_attachment;
|
||||
}
|
||||
|
||||
public function getFileContents()
|
||||
{
|
||||
return $this->getDisk()->get($this->getStorageFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
46
app/Console/Commands/CleanSendLog.php
Normal file
46
app/Console/Commands/CleanSendLog.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanSendLog extends Command
|
||||
{
|
||||
const PERIOD = '-6 months';
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:clean-send-log';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete old records from send log.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$logs = \App\SendLog::where('created_at', '<', \Carbon\Carbon::now()->modify(self::PERIOD))->delete();
|
||||
|
||||
$this->info('['.date('Y-m-d H:i:s').'] Deleted send logs: '.self::PERIOD);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,11 @@ class ClearCache extends Command
|
||||
$this->call('config:clear');
|
||||
} else {
|
||||
$this->call('config:cache');
|
||||
// Laravel users `require` function to include config.php
|
||||
// If opcache is being used for few seconds config.php is being cached.
|
||||
if (function_exists('opcache_invalidate')) {
|
||||
opcache_invalidate(app()->getCachedConfigPath());
|
||||
}
|
||||
}
|
||||
// Regenerate vars to get new data from .env
|
||||
if (!$this->option('doNotGenerateVars')) {
|
||||
|
||||
@@ -12,7 +12,7 @@ class CreateUser extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:create-user';
|
||||
protected $signature = 'freescout:create-user {--role=} {--firstName=} {--lastName=} {--email=} {--password=}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -46,40 +46,57 @@ class CreateUser extends Command
|
||||
).'.model'
|
||||
);
|
||||
$user = new $class();
|
||||
$fillables = ['role', 'first_name', 'last_name', 'email', 'password'];
|
||||
foreach ($fillables as $key => $fillable) {
|
||||
if ($fillable == 'password') {
|
||||
$user->$fillable = \Hash::make($this->secret(($key + 1).'/'.count($fillables)." User $fillable"));
|
||||
} elseif ($fillable == 'role') {
|
||||
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable (admin/user)", 'admin');
|
||||
if (!$user->$fillable) {
|
||||
$user->$fillable = 'admin';
|
||||
}
|
||||
|
||||
while (!in_array($user->$fillable, User::$roles)) {
|
||||
$this->error('Incorrect role');
|
||||
$user->$fillable = $this->ask('Please enter valid role');
|
||||
}
|
||||
$user->$fillable = array_flip(User::$roles)[$user->$fillable];
|
||||
} else {
|
||||
$user->$fillable = $this->ask(($key + 1).'/'.count($fillables)." User $fillable");
|
||||
|
||||
if ($fillable == 'email') {
|
||||
while (!filter_var($user->$fillable, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Incorrect email address');
|
||||
$user->$fillable = $this->ask('Please enter valid email address');
|
||||
}
|
||||
}
|
||||
$user->role = $this->option('role');
|
||||
if ($user->role ) {
|
||||
if (!in_array($user->role , User::$roles)) {
|
||||
$this->error('Invalid role');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$user->role = $this->ask('User role (admin/user)', 'admin');
|
||||
while (!in_array($user->role, User::$roles)) {
|
||||
$this->error('Invalid role');
|
||||
$user->role = $this->ask('Please enter valid role');
|
||||
}
|
||||
}
|
||||
$user->role = array_flip(User::$roles)[$user->role];
|
||||
|
||||
$user->first_name = $this->option('firstName') ? $this->option('firstName') : $this->ask('User first name');
|
||||
$user->last_name = $this->option('lastName') ? $this->option('lastName') : $this->ask('User last name');
|
||||
|
||||
$user->email = $this->option('email');
|
||||
if ($user->email) {
|
||||
if (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Invalid email address');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$user->email = $this->ask('User email address');
|
||||
while (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->error('Incorrect email address');
|
||||
$user->email = $this->ask('Please enter valid email address');
|
||||
}
|
||||
}
|
||||
|
||||
$user->password = \Hash::make($this->option('password') ? $this->option('password') : $this->secret('User password'));
|
||||
|
||||
if ($this->confirm('Do you want to create the user?', true)) {
|
||||
if ($user->isAdmin()) {
|
||||
$user->invite_state = User::INVITE_STATE_ACTIVATED;
|
||||
}
|
||||
$user->save();
|
||||
$this->info("User created (id: {$user->id})");
|
||||
|
||||
try {
|
||||
$user->save();
|
||||
} catch (\Exception $e) {
|
||||
$this->line($e->getMessage());
|
||||
$this->error('User already exists.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('User created with id: '.$user->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
61
app/Console/Commands/LogoutUsers.php
Normal file
61
app/Console/Commands/LogoutUsers.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class LogoutUsers extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'freescout:logout-users';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Logout all users';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
// Remove files from storage/frameworks/sessions
|
||||
$files = \File::files(storage_path('framework/sessions'));
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($files as $file) {
|
||||
try {
|
||||
$deleted = \File::delete($file->getPathname());
|
||||
if ($deleted) {
|
||||
$count++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
$this->line('Deleted sessions: '.$count);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,38 +43,60 @@ class ModuleCheckLicenses extends Command
|
||||
|
||||
$this->info('Active modules found: '.count($modules));
|
||||
|
||||
$params = [
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
'data' => [],
|
||||
];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
$license = $module->getLicense();
|
||||
|
||||
|
||||
if (!$module->isOfficial() || !$license) {
|
||||
continue;
|
||||
}
|
||||
$params = [
|
||||
'license' => $license,
|
||||
'module_alias' => $module->getAlias(),
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
];
|
||||
$result = WpApi::checkLicense($params);
|
||||
if (!empty($result['status']) && $result['status'] != 'valid') {
|
||||
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.json_encode($result);
|
||||
|
||||
$this->error($module->getName().': '.$msg);
|
||||
$data['license'] = $license;
|
||||
$data['module_alias'] = $module->getAlias();
|
||||
|
||||
// Deactive module
|
||||
\App\Module::deactiveModule($module->getAlias(), true);
|
||||
$params['data'][] = $data;
|
||||
}
|
||||
|
||||
// Inform admin
|
||||
\Log::error($msg);
|
||||
activity()
|
||||
->withProperties([
|
||||
'error' => $msg,
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_SYSTEM)
|
||||
->log(\App\ActivityLog::DESCRIPTION_SYSTEM_ERROR);
|
||||
} else {
|
||||
$this->info($module->getName().': OK');
|
||||
$result = WpApi::checkLicenses($params);
|
||||
|
||||
if (!empty($result['statuses'])) {
|
||||
foreach ($modules as $module) {
|
||||
$module_alias = $module->getAlias();
|
||||
|
||||
foreach ($result['statuses'] as $result_module_alias => $status) {
|
||||
if ($result_module_alias != $module_alias) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($status) && $status != 'valid') {
|
||||
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.$status;
|
||||
|
||||
$this->error($module->getName().': '.$msg);
|
||||
|
||||
// Deactive module
|
||||
\App\Module::deactiveModule($module->getAlias(), true);
|
||||
|
||||
// Inform admin
|
||||
\Log::error($msg);
|
||||
activity()
|
||||
->withProperties([
|
||||
'error' => $msg,
|
||||
])
|
||||
->useLog(\App\ActivityLog::NAME_SYSTEM)
|
||||
->log(\App\ActivityLog::DESCRIPTION_SYSTEM_ERROR);
|
||||
} else {
|
||||
$this->info($module->getName().': OK');
|
||||
}
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$this->info($module->getName().': Unknown status');
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Checking licenses finished');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +46,21 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->command('freescout:update-folder-counters')
|
||||
->hourly();
|
||||
|
||||
$schedule->command('freescout:module-check-licenses')
|
||||
->daily();
|
||||
$app_key = config('app.key');
|
||||
if ($app_key) {
|
||||
$crc = crc32($app_key);
|
||||
$schedule->command('freescout:module-check-licenses')
|
||||
->cron((int)($crc % 59).' '.(int)($crc % 23).' * * *');
|
||||
}
|
||||
|
||||
// Check if user finished viewing conversation.
|
||||
$schedule->command('freescout:check-conv-viewers')
|
||||
->everyMinute()
|
||||
->withoutOverlapping();
|
||||
|
||||
$schedule->command('freescout:clean-send-log')
|
||||
->monthly();
|
||||
|
||||
// Logs monitoring.
|
||||
$alert_logs_period = config('app.alert_logs_period');
|
||||
if (config('app.alert_logs') && $alert_logs_period) {
|
||||
@@ -180,6 +187,13 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
// Swiftmailer uses $_SERVER['SERVER_NAME'] in transport_deps.php
|
||||
// to set the host for EHLO command, if it is empty it uses [127.0.0.1].
|
||||
// G Suite sometimes rejects emails with EHLO [127.0.0.1].
|
||||
if (empty($_SERVER['SERVER_NAME'])) {
|
||||
$_SERVER['SERVER_NAME'] = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Attachment;
|
||||
use App\Customer;
|
||||
use App\Mailbox;
|
||||
use App\Folder;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
use App\Events\UserAddedNote;
|
||||
@@ -19,12 +22,17 @@ class Conversation extends Model
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
|
||||
/**
|
||||
* Max length of the preview.
|
||||
*/
|
||||
const PREVIEW_MAXLENGTH = 255;
|
||||
|
||||
/**
|
||||
* Default subject length.
|
||||
*/
|
||||
const SUBJECT_LENGTH = 80;
|
||||
|
||||
/**
|
||||
* Conversation reply undo timeout in seconds.
|
||||
* Value has to be larger than close_after in fsFloatingAlertsInit.
|
||||
@@ -47,7 +55,7 @@ class Conversation extends Model
|
||||
*/
|
||||
const TYPE_EMAIL = 1;
|
||||
const TYPE_PHONE = 2;
|
||||
const TYPE_CHAT = 3; // not used
|
||||
const TYPE_CHAT = 3;
|
||||
|
||||
public static $types = [
|
||||
self::TYPE_EMAIL => 'email',
|
||||
@@ -125,6 +133,21 @@ class Conversation extends Model
|
||||
self::SOURCE_TYPE_API => 'api',
|
||||
];
|
||||
|
||||
/**
|
||||
* Email history options.
|
||||
*/
|
||||
// const EMAIL_HISTORY_GLOBAL = 0;
|
||||
// const EMAIL_HISTORY_NONE = 1;
|
||||
// const EMAIL_HISTORY_LAST = 2;
|
||||
// const EMAIL_HISTORY_FULL = 3;
|
||||
|
||||
public static $email_history_codes = [
|
||||
'global',
|
||||
'none',
|
||||
'last',
|
||||
'full',
|
||||
];
|
||||
|
||||
/**
|
||||
* Assignee.
|
||||
*/
|
||||
@@ -138,6 +161,7 @@ class Conversation extends Model
|
||||
'customer',
|
||||
'mailbox',
|
||||
'status',
|
||||
'state',
|
||||
'subject',
|
||||
'attachments',
|
||||
'type',
|
||||
@@ -167,7 +191,7 @@ class Conversation extends Model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $starred_conversation_ids = null;
|
||||
public static $starred_conversation_ids = [];
|
||||
|
||||
/**
|
||||
* Automatically converted into Carbon dates.
|
||||
@@ -231,6 +255,15 @@ class Conversation extends Model
|
||||
return $this->belongsTo('App\Mailbox');
|
||||
}
|
||||
|
||||
/**
|
||||
* Chached mailbox.
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function mailbox_cached()
|
||||
{
|
||||
return $this->mailbox()->rememberForever();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer associated with this conversation (primaryCustomer).
|
||||
*/
|
||||
@@ -326,7 +359,7 @@ class Conversation extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all published conversation thread in desc order.
|
||||
* Get all published conversation threads in desc order.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
@@ -366,10 +399,14 @@ class Conversation extends Model
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getLastReply()
|
||||
public function getLastReply($include_phone_replies = false)
|
||||
{
|
||||
$types = [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE];
|
||||
if ($include_phone_replies && $this->isPhone()) {
|
||||
$types[] = Thread::TYPE_NOTE;
|
||||
}
|
||||
return $this->threads()
|
||||
->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE])
|
||||
->whereIn('type', $types)
|
||||
->where('state', Thread::STATE_PUBLISHED)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
@@ -445,6 +482,11 @@ class Conversation extends Model
|
||||
return $this->status == self::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
public function isSpam()
|
||||
{
|
||||
return $this->status == self::STATUS_SPAM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status name.
|
||||
*
|
||||
@@ -491,6 +533,34 @@ class Conversation extends Model
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert state code to name.
|
||||
*
|
||||
* @param int $status
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function stateCodeToName($status)
|
||||
{
|
||||
switch ($status) {
|
||||
case self::STATE_DRAFT:
|
||||
return __('Draft');
|
||||
break;
|
||||
|
||||
case self::STATE_PUBLISHED:
|
||||
return __('Published');
|
||||
break;
|
||||
|
||||
case self::STATE_DELETED:
|
||||
return __('Deleted');
|
||||
break;
|
||||
|
||||
default:
|
||||
return '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set conersation status and all related fields.
|
||||
*
|
||||
@@ -544,8 +614,12 @@ class Conversation extends Model
|
||||
} else {
|
||||
$folder = $this->folder;
|
||||
}
|
||||
$query = self::where('folder_id', $folder->id)
|
||||
//$query = self::where('folder_id', $folder->id)->where('id', '<>', $this->id);
|
||||
$query = self::getQueryByFolder($folder, \Auth::id())
|
||||
->where('id', '<>', $this->id);
|
||||
|
||||
$query = \Eventy::filter('conversation.get_nearby_query', $query, $this, $mode, $folder);
|
||||
|
||||
$order_bys = $folder->getOrderByArray();
|
||||
|
||||
if ($mode != 'prev') {
|
||||
@@ -717,7 +791,33 @@ class Conversation extends Model
|
||||
*/
|
||||
public static function sanitizeEmails($emails)
|
||||
{
|
||||
return \App\Misc\Mail::sanitizeEmails($emails);
|
||||
// Create customers if needed: Test <test1@example.com>
|
||||
if (is_array($emails)) {
|
||||
foreach ($emails as $i => $email) {
|
||||
preg_match("/^(.+)\s+([^\s]+)$/", $email ?? '', $m);
|
||||
if (count($m) == 3) {
|
||||
$customer_name = trim($m[1]);
|
||||
$email_address = trim($m[2]);
|
||||
|
||||
if ($customer_name) {
|
||||
preg_match("/^([^\s]+)\s+([^\s]+)$/", $customer_name, $m_customer);
|
||||
$customer_data = [];
|
||||
|
||||
if (count($m_customer) == 3) {
|
||||
$customer_data['first_name'] = $m_customer[1];
|
||||
$customer_data['last_name'] = $m_customer[2];
|
||||
} else {
|
||||
$customer_data['first_name'] = $customer_name;
|
||||
}
|
||||
|
||||
Customer::create($email_address, $customer_data);
|
||||
}
|
||||
|
||||
$emails[$i] = $email_address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return \MailHelper::sanitizeEmails($emails);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -735,7 +835,7 @@ class Conversation extends Model
|
||||
|
||||
/**
|
||||
* Static function for retrieving URL.
|
||||
*
|
||||
*
|
||||
* @param [type] $id [description]
|
||||
* @param [type] $folder_id [description]
|
||||
* @param [type] $thread_id [description]
|
||||
@@ -810,6 +910,7 @@ class Conversation extends Model
|
||||
}
|
||||
} else {
|
||||
// todo: check ConversationFolder here
|
||||
return \Eventy::filter('conversation.is_in_folder_allowed', false, $folder, $this);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -829,14 +930,16 @@ class Conversation extends Model
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$mailbox_id = $this->mailbox_id;
|
||||
|
||||
// Get ids of all the conversations starred by user and cache them
|
||||
if (self::$starred_conversation_ids === null) {
|
||||
$mailbox_id = $this->mailbox_id;
|
||||
self::$starred_conversation_ids = self::getUserStarredConversationIds($mailbox_id, $user_id);
|
||||
if (!isset(self::$starred_conversation_ids[$mailbox_id])) {
|
||||
|
||||
self::$starred_conversation_ids[$mailbox_id] = self::getUserStarredConversationIds($mailbox_id, $user_id);
|
||||
}
|
||||
|
||||
if (self::$starred_conversation_ids) {
|
||||
return in_array($this->id, self::$starred_conversation_ids);
|
||||
if (self::$starred_conversation_ids[$mailbox_id]) {
|
||||
return in_array($this->id, self::$starred_conversation_ids[$mailbox_id]);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -955,7 +1058,9 @@ class Conversation extends Model
|
||||
*/
|
||||
public function getSignatureProcessed($data = [], $escape = false)
|
||||
{
|
||||
return $this->replaceTextVars($this->mailbox->signature, $data, $escape);
|
||||
$replaced_text = $this->replaceTextVars( $this->mailbox->signature, $data, $escape );
|
||||
|
||||
return \Eventy::filter( 'conversation.signature_processed', $replaced_text, $this, $data, $escape );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -963,7 +1068,7 @@ class Conversation extends Model
|
||||
*/
|
||||
public function replaceTextVars($text, $data = [], $escape = false)
|
||||
{
|
||||
if (!\App\Misc\Mail::hasVars($text)) {
|
||||
if (!\MailHelper::hasVars($text)) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
@@ -1005,6 +1110,10 @@ class Conversation extends Model
|
||||
}
|
||||
}
|
||||
|
||||
if (!$customer_email) {
|
||||
$customer_email = $customer->getMainEmail();
|
||||
}
|
||||
|
||||
$prev_customer_id = $this->customer_id;
|
||||
$prev_customer_email = $this->customer_email;
|
||||
|
||||
@@ -1041,16 +1150,57 @@ class Conversation extends Model
|
||||
{
|
||||
$prev_mailbox = $this->mailbox;
|
||||
|
||||
foreach ($this->folders as $folder) {
|
||||
// Process indirect folders.
|
||||
if (!in_array($folder->type, Folder::$indirect_types)) {
|
||||
continue;
|
||||
}
|
||||
// Remove conversation from the folder.
|
||||
$this->removeFromFolder($folder->type, $folder->user_id);
|
||||
if ($folder->type == Folder::TYPE_STARRED) {
|
||||
self::clearStarredByUserCache($folder->user_id, $this->mailbox_id);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't know how to replace $this->mailbox object.
|
||||
$this->mailbox_id = $mailbox->id;
|
||||
// Check assignee.
|
||||
if ($this->user_id && !in_array($this->user_id, $mailbox->userIdsHavingAccess())) {
|
||||
// Assign conversation to the user who moved it.
|
||||
$conversation->user_id = $user->id;
|
||||
$this->user_id = $user->id;
|
||||
}
|
||||
$this->updateFolder($mailbox);
|
||||
$this->save();
|
||||
|
||||
foreach ($this->folders as $folder) {
|
||||
// Process indirect folders.
|
||||
if (!in_array($folder->type, Folder::$indirect_types)) {
|
||||
continue;
|
||||
}
|
||||
// If user has access to the new mailbox,
|
||||
// move conversation to the same folder in the new mailbox.
|
||||
if ($folder->user_id) {
|
||||
if ($folder->user->hasAccessToMailbox($mailbox->id)) {
|
||||
foreach ($mailbox->folders as $mailbox_folder) {
|
||||
if ($mailbox_folder->type == $folder->type) {
|
||||
$this->addToFolder($folder->type, $folder->user_id);
|
||||
if ($folder->type == Folder::TYPE_STARRED) {
|
||||
self::clearStarredByUserCache($folder->user_id, $mailbox->id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($mailbox->folders as $mailbox_folder) {
|
||||
if ($mailbox_folder->type == $folder->type) {
|
||||
$this->addToFolder($folder->type, $folder->user_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add record to the conversation history.
|
||||
Thread::create($this, Thread::TYPE_LINEITEM, '', [
|
||||
'created_by_user_id' => $user->id,
|
||||
@@ -1066,6 +1216,57 @@ class Conversation extends Model
|
||||
$prev_mailbox->updateFoldersCounters();
|
||||
$mailbox->updateFoldersCounters();
|
||||
|
||||
\Eventy::action('conversation.moved', $this, $user, $prev_mailbox);
|
||||
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Merge conversations
|
||||
*/
|
||||
public function mergeConversations($merge_conversation, $user)
|
||||
{
|
||||
// Move all threads from old to new conversation.
|
||||
foreach ($merge_conversation->threads as $thread) {
|
||||
$thread->conversation_id = $this->id;
|
||||
$thread->setMeta(Thread::META_PREV_CONVERSATION, $merge_conversation->id);
|
||||
$thread->save();
|
||||
}
|
||||
|
||||
// Add record to the new conversation.
|
||||
Thread::create($this, Thread::TYPE_LINEITEM, '', [
|
||||
'created_by_user_id' => $user->id,
|
||||
'user_id' => $this->user_id,
|
||||
'state' => Thread::STATE_PUBLISHED,
|
||||
'action_type' => Thread::ACTION_TYPE_MERGED,
|
||||
'source_via' => Thread::PERSON_USER,
|
||||
'source_type' => Thread::SOURCE_TYPE_WEB,
|
||||
'customer_id' => $this->customer_id,
|
||||
'meta' => [Thread::META_MERGED_WITH_CONV => $merge_conversation->id],
|
||||
]);
|
||||
|
||||
// Add record to the old conversation.
|
||||
Thread::create($merge_conversation, Thread::TYPE_LINEITEM, '', [
|
||||
'created_by_user_id' => $user->id,
|
||||
'user_id' => $merge_conversation->user_id,
|
||||
'state' => Thread::STATE_PUBLISHED,
|
||||
'action_type' => Thread::ACTION_TYPE_MERGED,
|
||||
'source_via' => Thread::PERSON_USER,
|
||||
'source_type' => Thread::SOURCE_TYPE_WEB,
|
||||
'customer_id' => $merge_conversation->customer_id,
|
||||
'meta' => [Thread::META_MERGED_INTO_CONV => $this->id],
|
||||
]);
|
||||
|
||||
// Delete old conversation.
|
||||
$merge_conversation->deleteToFolder($user);
|
||||
|
||||
// Update counters.
|
||||
$this->mailbox->updateFoldersCounters();
|
||||
if ($this->mailbox_id != $merge_conversation->mailbox_id) {
|
||||
$merge_conversation->mailbox->updateFoldersCounters();
|
||||
}
|
||||
|
||||
\Eventy::action('conversation.merged', $this, $merge_conversation, $user);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1075,7 +1276,7 @@ class Conversation extends Model
|
||||
public static function loadUsers($conversations)
|
||||
{
|
||||
$user_ids = $conversations->pluck('user_id')->unique()->toArray();
|
||||
if (!$user_ids) {
|
||||
if (!$user_ids || (count($user_ids) == 1 && empty($user_ids[0]))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1127,6 +1328,35 @@ class Conversation extends Model
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load mailboxes.
|
||||
*/
|
||||
public static function loadMailboxes($conversations)
|
||||
{
|
||||
$ids = $conversations->pluck('mailbox_id')->unique()->toArray();
|
||||
if (!$ids) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mailboxes = Mailbox::whereIn('id', $ids)->get();
|
||||
if (!$mailboxes) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($conversations as $conversation) {
|
||||
if (empty($conversation->mailbox_id)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
if ($mailbox->id == $conversation->mailbox_id) {
|
||||
$conversation->mailbox = $mailbox;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getSubject()
|
||||
{
|
||||
if ($this->subject) {
|
||||
@@ -1139,12 +1369,16 @@ class Conversation extends Model
|
||||
/**
|
||||
* Add conversation to folder via conversation_folder table.
|
||||
*/
|
||||
public function addToFolder($folder_type)
|
||||
public function addToFolder($folder_type, $user_id = null)
|
||||
{
|
||||
// Find folder
|
||||
$folder = Folder::where('mailbox_id', $this->mailbox_id)
|
||||
->where('type', $folder_type)
|
||||
->first();
|
||||
// Find folder.
|
||||
$folder_query = Folder::where('mailbox_id', $this->mailbox_id)
|
||||
->where('type', $folder_type);
|
||||
if ($user_id) {
|
||||
$folder_query->where('user_id', $user_id);
|
||||
}
|
||||
$folder = $folder_query->first();
|
||||
|
||||
if (!$folder) {
|
||||
return false;
|
||||
}
|
||||
@@ -1166,20 +1400,32 @@ class Conversation extends Model
|
||||
// 'conversation_id' => $this->id,
|
||||
// ];
|
||||
// ConversationFolder::updateOrCreate($values, $values);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeFromFolder($folder_type)
|
||||
/**
|
||||
* When removing from Starred folder, don't forget to clear cache using clearStarredByUserCache()
|
||||
*/
|
||||
public function removeFromFolder($folder_type, $user_id = null)
|
||||
{
|
||||
// Find folder
|
||||
$folder = Folder::where('mailbox_id', $this->mailbox_id)
|
||||
->where('type', $folder_type)
|
||||
->first();
|
||||
$folder_query = Folder::where('mailbox_id', $this->mailbox_id)
|
||||
->where('type', $folder_type);
|
||||
|
||||
if ($user_id) {
|
||||
$folder_query->where('user_id', $user_id);
|
||||
}
|
||||
$folder = $folder_query->first();
|
||||
|
||||
if (!$folder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->folders()->detach($folder->id);
|
||||
$folder->updateCounters();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1217,10 +1463,17 @@ class Conversation extends Model
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getWaitingSince($folder)
|
||||
public function getWaitingSince($folder = null)
|
||||
{
|
||||
if (!$folder) {
|
||||
$folder = $this->folder;
|
||||
}
|
||||
$waiting_since_field = $folder->getWaitingSinceField();
|
||||
if ($waiting_since_field) {
|
||||
// For phone conversations.
|
||||
if (empty($this->$waiting_since_field)) {
|
||||
$waiting_since_field = 'updated_at';
|
||||
}
|
||||
return \App\User::dateDiffForHumans($this->$waiting_since_field);
|
||||
} else {
|
||||
return '';
|
||||
@@ -1272,7 +1525,7 @@ class Conversation extends Model
|
||||
$mailbox = $this->mailbox;
|
||||
}
|
||||
$customer_emails = [$this->customer_email];
|
||||
if (strstr($this->customer_email, ',')) {
|
||||
if (strstr($this->customer_email ?? '', ',')) {
|
||||
// customer_email contains mutiple addresses (when new conversation for multiple recipients created)
|
||||
$customer_emails = explode(',', $this->customer_email);
|
||||
}
|
||||
@@ -1287,6 +1540,14 @@ class Conversation extends Model
|
||||
return ($this->type == self::TYPE_PHONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it as chat conversation.
|
||||
*/
|
||||
public function isChat()
|
||||
{
|
||||
return ($this->type == self::TYPE_CHAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information on viewers for conversation table.
|
||||
*/
|
||||
@@ -1342,6 +1603,10 @@ class Conversation extends Model
|
||||
|
||||
public function changeStatus($new_status, $user, $create_thread = true)
|
||||
{
|
||||
if (!array_key_exists($new_status, self::$statuses)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$prev_status = $this->status;
|
||||
|
||||
$this->setStatus($new_status, $user);
|
||||
@@ -1371,24 +1636,26 @@ class Conversation extends Model
|
||||
public function changeUser($new_user_id, $user, $create_thread = true)
|
||||
{
|
||||
$prev_user_id = $this->user_id;
|
||||
|
||||
|
||||
$this->setUser($new_user_id);
|
||||
$this->save();
|
||||
|
||||
// Create lineitem thread
|
||||
$thread = new Thread();
|
||||
$thread->conversation_id = $this->id;
|
||||
$thread->user_id = $this->user_id;
|
||||
$thread->type = Thread::TYPE_LINEITEM;
|
||||
$thread->state = Thread::STATE_PUBLISHED;
|
||||
$thread->status = Thread::STATUS_NOCHANGE;
|
||||
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
|
||||
$thread->source_via = Thread::PERSON_USER;
|
||||
// todo: this need to be changed for API
|
||||
$thread->source_type = Thread::SOURCE_TYPE_WEB;
|
||||
$thread->customer_id = $this->customer_id;
|
||||
$thread->created_by_user_id = $user->id;
|
||||
$thread->save();
|
||||
if ($create_thread) {
|
||||
// Create lineitem thread
|
||||
$thread = new Thread();
|
||||
$thread->conversation_id = $this->id;
|
||||
$thread->user_id = $this->user_id;
|
||||
$thread->type = Thread::TYPE_LINEITEM;
|
||||
$thread->state = Thread::STATE_PUBLISHED;
|
||||
$thread->status = Thread::STATUS_NOCHANGE;
|
||||
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
|
||||
$thread->source_via = Thread::PERSON_USER;
|
||||
// todo: this need to be changed for API
|
||||
$thread->source_type = Thread::SOURCE_TYPE_WEB;
|
||||
$thread->customer_id = $this->customer_id;
|
||||
$thread->created_by_user_id = $user->id;
|
||||
$thread->save();
|
||||
}
|
||||
|
||||
event(new ConversationUserChanged($this, $user));
|
||||
\Eventy::action('conversation.user_changed', $this, $user, $prev_user_id);
|
||||
@@ -1397,7 +1664,7 @@ class Conversation extends Model
|
||||
public function deleteToFolder($user)
|
||||
{
|
||||
$folder_id = $this->getCurrentFolder();
|
||||
|
||||
|
||||
$this->state = Conversation::STATE_DELETED;
|
||||
$this->user_updated_at = date('Y-m-d H:i:s');
|
||||
$this->updateFolder();
|
||||
@@ -1423,6 +1690,8 @@ class Conversation extends Model
|
||||
|
||||
// Recalculate only old and new folders
|
||||
$this->mailbox->updateFoldersCounters();
|
||||
|
||||
\Eventy::action('conversation.deleted', $this, $user);
|
||||
}
|
||||
|
||||
public function deleteForever()
|
||||
@@ -1478,7 +1747,7 @@ class Conversation extends Model
|
||||
}
|
||||
}
|
||||
|
||||
public function forward($user, $body, $to = '', $data = [])
|
||||
public function forward($user, $body, $to = '', $data = [], $include_attachments = false)
|
||||
{
|
||||
// Create thread
|
||||
$thread = Thread::create($this, $data['type'] ?? Thread::TYPE_NOTE, $body, $data, false);
|
||||
@@ -1512,9 +1781,6 @@ class Conversation extends Model
|
||||
$forwarded_conversation->last_reply_at = $now;
|
||||
$forwarded_conversation->last_reply_from = Conversation::PERSON_USER;
|
||||
$forwarded_conversation->user_updated_at = $now;
|
||||
// if ($attachments_info['has_attachments']) {
|
||||
// $forwarded_conversation->has_attachments = true;
|
||||
// }
|
||||
$forwarded_conversation->updateFolder();
|
||||
$forwarded_conversation->save();
|
||||
|
||||
@@ -1524,7 +1790,7 @@ class Conversation extends Model
|
||||
$thread->subtype = Thread::SUBTYPE_FORWARD;
|
||||
$thread->setMeta('forward_child_conversation_number', $forwarded_conversation->number);
|
||||
$thread->setMeta('forward_child_conversation_id', $forwarded_conversation->id);
|
||||
|
||||
|
||||
$thread->save();
|
||||
|
||||
// Save forwarded thread.
|
||||
@@ -1540,6 +1806,52 @@ class Conversation extends Model
|
||||
$forwarded_thread->setMeta('forward_parent_thread_id', $thread->id);
|
||||
$forwarded_thread->save();
|
||||
|
||||
// Add attachments if needed.
|
||||
if ($include_attachments) {
|
||||
|
||||
$replies = $this->getReplies();
|
||||
|
||||
$has_attachments = false;
|
||||
foreach ($replies as $reply_thread) {
|
||||
|
||||
$thread_has_attachments = false;
|
||||
foreach ($reply_thread->attachments as $attachment) {
|
||||
$new_attachment = $attachment->replicate();
|
||||
$new_attachment->thread_id = $forwarded_thread->id;
|
||||
// We need to copy attachment file, because conversations
|
||||
// can be deleted along with attachments.
|
||||
$new_attachment->save();
|
||||
|
||||
try {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$attachment->getLocalFilePath(), $attachment->file_name,
|
||||
null, null, true
|
||||
);
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
|
||||
|
||||
if (!empty($file_info['file_dir'])) {
|
||||
$new_attachment->file_dir = $file_info['file_dir'];
|
||||
$new_attachment->save();
|
||||
|
||||
$has_attachments = true;
|
||||
$thread_has_attachments = true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e);
|
||||
}
|
||||
}
|
||||
if ($thread_has_attachments) {
|
||||
$forwarded_thread->has_attachments = true;
|
||||
$forwarded_thread->save();
|
||||
}
|
||||
}
|
||||
if ($has_attachments) {
|
||||
$forwarded_conversation->has_attachments = true;
|
||||
$forwarded_conversation->save();
|
||||
}
|
||||
}
|
||||
|
||||
// Update folders counters
|
||||
$this->mailbox->updateFoldersCounters();
|
||||
|
||||
@@ -1550,6 +1862,287 @@ class Conversation extends Model
|
||||
\Eventy::action('conversation.user_forwarded', $this, $thread, $forwarded_conversation, $forwarded_thread);
|
||||
}
|
||||
|
||||
// public function getEmailHistoryCode()
|
||||
// {
|
||||
// return self::$email_history_codes[(int)$this->email_history] ?? 'global';
|
||||
// }
|
||||
|
||||
public static function getEmailHistoryName($code) {
|
||||
$label = '';
|
||||
|
||||
switch ($code) {
|
||||
case 'global':
|
||||
$label = __('Default');
|
||||
$label .= ' ('.self::getEmailHistoryName(config('app.email_conv_history')).')';
|
||||
break;
|
||||
case 'none':
|
||||
$label = __('Do not include previous messages');
|
||||
break;
|
||||
case 'last':
|
||||
$label = __('Include the last message');
|
||||
break;
|
||||
case 'full':
|
||||
$label = __('Send full conversation history');
|
||||
break;
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create conversation.
|
||||
*
|
||||
* $threads should go from old to new.
|
||||
*/
|
||||
public static function create($data, $threads, $customer)
|
||||
{
|
||||
// Detect source_via.
|
||||
$source_via = $data['source_via'] ?? 0;
|
||||
if (!$source_via && !empty($threads[0])) {
|
||||
if (!empty($threads[0]['type']) && $threads[0]['type'] == Thread::TYPE_CUSTOMER) {
|
||||
$source_via = self::PERSON_CUSTOMER;
|
||||
} else {
|
||||
$source_via = self::PERSON_USER;
|
||||
}
|
||||
}
|
||||
|
||||
$conversation = new Conversation();
|
||||
$conversation->type = $data['type'];
|
||||
$conversation->subject = $data['subject'];
|
||||
$conversation->mailbox_id = $data['mailbox_id'];
|
||||
$conversation->source_via = $source_via;
|
||||
$conversation->source_type = $data['source_type'];
|
||||
$conversation->customer_id = $customer->id;
|
||||
$conversation->customer_email = $customer->getMainEmail().'';
|
||||
$conversation->state = $data['state'] ?? Conversation::STATE_PUBLISHED;
|
||||
$conversation->imported = (int)($data['imported'] ?? false);
|
||||
$conversation->closed_at = $data['closed_at'] ?? null;
|
||||
$conversation->channel = $data['channel'] ?? null;
|
||||
$conversation->preview = '';
|
||||
|
||||
// Phone conversation is always pending.
|
||||
if ($conversation->isPhone()) {
|
||||
$conversation->status = Conversation::STATUS_PENDING;
|
||||
}
|
||||
|
||||
// Set assignee
|
||||
$conversation->user_id = null;
|
||||
if (!empty($data['user_id'])) {
|
||||
$user_assignee = User::find($data['user_id']);
|
||||
if ($user_assignee) {
|
||||
$conversation->user_id = $user_assignee->id;
|
||||
}
|
||||
}
|
||||
|
||||
$conversation->updateFolder();
|
||||
$conversation->save();
|
||||
|
||||
// Create threads.
|
||||
$threads = array_reverse($threads);
|
||||
$thread_created = false;
|
||||
$last_customer_id = null;
|
||||
$thread_result = null;
|
||||
foreach ($threads as $thread) {
|
||||
|
||||
$thread['conversation_id'] = $conversation->id;
|
||||
|
||||
if ($conversation->imported) {
|
||||
$thread['imported'] = true;
|
||||
}
|
||||
if (!empty($data['status'])) {
|
||||
$thread['status'] = $data['status'];
|
||||
}
|
||||
|
||||
$thread_result = Thread::createExtended($thread, $conversation, $customer, false);
|
||||
if ($thread_result) {
|
||||
$thread_created = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no threads created, delete conversation
|
||||
if (!$thread_created) {
|
||||
$conversation->delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restore customer if needed.
|
||||
// if ($last_customer_id && $last_customer_id != $customer->id) {
|
||||
// // Otherwise it does not save.
|
||||
// $conversation = $conversation->fresh();
|
||||
// $conversation->customer_id = $customer->id;
|
||||
// $conversation->customer_email = $customer->getMainEmail();
|
||||
// $conversation->save();
|
||||
// }
|
||||
|
||||
// Update folders counters
|
||||
$conversation->mailbox->updateFoldersCounters();
|
||||
|
||||
return [
|
||||
'conversation' => $conversation,
|
||||
'thread' => $thread_result
|
||||
];
|
||||
}
|
||||
|
||||
public function getChannelName()
|
||||
{
|
||||
return self::channelCodeToName($this->channel);
|
||||
}
|
||||
|
||||
public static function channelCodeToName($channel)
|
||||
{
|
||||
return \Eventy::filter('channel.name', '', $channel);
|
||||
}
|
||||
|
||||
public static function subjectFromText($text)
|
||||
{
|
||||
return \Helper::textPreview($text, self::SUBJECT_LENGTH);
|
||||
}
|
||||
|
||||
public static function refreshConversations($conversation, $thread)
|
||||
{
|
||||
\App\Events\RealtimeConvNewThread::dispatchSelf($thread);
|
||||
\App\Events\RealtimeMailboxNewThread::dispatchSelf($conversation->mailbox_id);
|
||||
}
|
||||
|
||||
public static function getConvTableSorting($request = null)
|
||||
{
|
||||
if (!$request) {
|
||||
$request = request();
|
||||
}
|
||||
|
||||
$result = [
|
||||
'sort_by' => 'date',
|
||||
'order' => 'desc',
|
||||
];
|
||||
|
||||
if (
|
||||
!empty($request->sorting['sort_by']) && !empty($request->sorting['order']) &&
|
||||
in_array($request->sorting['sort_by'], ['subject', 'number', 'date']) &&
|
||||
in_array($request->sorting['order'], ['asc', 'desc'])
|
||||
) {
|
||||
$result['sort_by'] = $request->sorting['sort_by'];
|
||||
$result['order'] = $request->sorting['order'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function search($q, $filters, $user = null, $query_conversations = null)
|
||||
{
|
||||
$mailbox_ids = [];
|
||||
|
||||
// Get IDs of mailboxes to which user has access
|
||||
if (empty($filters['mailbox'])) {
|
||||
$mailbox_ids = $user->mailboxesIdsCanView();
|
||||
}
|
||||
|
||||
// Like is case insensitive.
|
||||
$like = '%'.mb_strtolower($q).'%';
|
||||
|
||||
if (!$query_conversations) {
|
||||
$query_conversations = Conversation::select('conversations.*');
|
||||
}
|
||||
// https://github.com/laravel/framework/issues/21242
|
||||
// https://github.com/laravel/framework/pull/27675
|
||||
$query_conversations->groupby('conversations.id');
|
||||
|
||||
if ($mailbox_ids) {
|
||||
$query_conversations->whereIn('conversations.mailbox_id', $mailbox_ids);
|
||||
}
|
||||
if ($q) {
|
||||
$query_conversations->where(function ($query) use ($like, $filters, $q) {
|
||||
$query->where('conversations.subject', 'like', $like)
|
||||
->orWhere('conversations.customer_email', 'like', $like)
|
||||
->orWhere('conversations.number', (int)$q)
|
||||
->orWhere('conversations.id', (int)$q)
|
||||
->orWhere('threads.body', 'like', $like)
|
||||
->orWhere('threads.from', 'like', $like)
|
||||
->orWhere('threads.to', 'like', $like)
|
||||
->orWhere('threads.cc', 'like', $like)
|
||||
->orWhere('threads.bcc', 'like', $like);
|
||||
|
||||
$query = \Eventy::filter('search.conversations.or_where', $query, $filters, $q);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply search filters.
|
||||
if (!empty($filters['assigned'])) {
|
||||
$query_conversations->where('conversations.user_id', $filters['assigned']);
|
||||
}
|
||||
if (!empty($filters['customer'])) {
|
||||
$customer_id = $filters['customer'];
|
||||
$query_conversations->where(function ($query) use ($customer_id) {
|
||||
$query->where('conversations.customer_id', '=', $customer_id)
|
||||
->orWhere('threads.created_by_customer_id', '=', $customer_id);
|
||||
});
|
||||
}
|
||||
if (!empty($filters['mailbox'])) {
|
||||
$query_conversations->where('conversations.mailbox_id', '=', $filters['mailbox']);
|
||||
}
|
||||
if (!empty($filters['status'])) {
|
||||
if (count($filters['status']) == 1) {
|
||||
// = is faster than IN.
|
||||
$query_conversations->where('conversations.status', '=', $filters['status'][0]);
|
||||
} else {
|
||||
$query_conversations->whereIn('conversations.status', $filters['status']);
|
||||
}
|
||||
}
|
||||
if (!empty($filters['state'])) {
|
||||
if (count($filters['state']) == 1) {
|
||||
// = is faster than IN.
|
||||
$query_conversations->where('conversations.state', '=', $filters['state'][0]);
|
||||
} else {
|
||||
$query_conversations->whereIn('conversations.state', $filters['state']);
|
||||
}
|
||||
}
|
||||
if (!empty($filters['subject'])) {
|
||||
$query_conversations->where('conversations.subject', 'like', '%'.mb_strtolower($filters['subject']).'%');
|
||||
}
|
||||
if (!empty($filters['attachments'])) {
|
||||
$has_attachments = ($filters['attachments'] == 'yes' ? true : false);
|
||||
$query_conversations->where('conversations.has_attachments', '=', $has_attachments);
|
||||
}
|
||||
if (!empty($filters['type'])) {
|
||||
$query_conversations->where('conversations.type', '=', $filters['type']);
|
||||
}
|
||||
if (!empty($filters['body'])) {
|
||||
$query_conversations->where('threads.body', 'like', '%'.mb_strtolower($filters['body']).'%');
|
||||
}
|
||||
if (!empty($filters['number'])) {
|
||||
$query_conversations->where('conversations.number', '=', $filters['number']);
|
||||
}
|
||||
if (!empty($filters['following'])) {
|
||||
if ($filters['following'] == 'yes') {
|
||||
$query_conversations->join('followers', function ($join) {
|
||||
$join->on('followers.conversation_id', '=', 'conversations.id');
|
||||
$join->where('followers.user_id', auth()->user()->id);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!empty($filters['id'])) {
|
||||
$query_conversations->where('conversations.id', '=', $filters['id']);
|
||||
}
|
||||
if (!empty($filters['after'])) {
|
||||
$query_conversations->where('conversations.created_at', '>=', date('Y-m-d 00:00:00', strtotime($filters['after'])));
|
||||
}
|
||||
if (!empty($filters['before'])) {
|
||||
$query_conversations->where('conversations.created_at', '<=', date('Y-m-d 23:59:59', strtotime($filters['before'])));
|
||||
}
|
||||
|
||||
// Join threads if needed
|
||||
if (!strstr($query_conversations->toSql(), '`threads`.`conversation_id`')) {
|
||||
$query_conversations->join('threads', function ($join) {
|
||||
$join->on('conversations.id', '=', 'threads.conversation_id');
|
||||
});
|
||||
}
|
||||
|
||||
$query_conversations = \Eventy::filter('search.conversations.apply_filters', $query_conversations, $filters, $q);
|
||||
|
||||
$query_conversations->orderBy('conversations.last_reply_at', 'DESC');
|
||||
|
||||
return $query_conversations;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Get conversation meta data as array.
|
||||
// */
|
||||
|
||||
593
app/Customer.php
593
app/Customer.php
@@ -2,7 +2,10 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Email;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
|
||||
class Customer extends Model
|
||||
@@ -11,6 +14,10 @@ class Customer extends Model
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
const PHOTO_DIRECTORY = 'customers';
|
||||
const PHOTO_SIZE = 64; // px
|
||||
const PHOTO_QUALITY = 77;
|
||||
|
||||
/**
|
||||
* Genders.
|
||||
*/
|
||||
@@ -43,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_FAX => 'fax',
|
||||
self::PHONE_TYPE_PAGER => 'pager',
|
||||
self::PHONE_TYPE_OTHER => 'other',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -78,51 +85,52 @@ class Customer extends Model
|
||||
/**
|
||||
* Chat types.
|
||||
*/
|
||||
const CHAT_TYPE_AIM = 1;
|
||||
const CHAT_TYPE_GTALK = 2;
|
||||
const CHAT_TYPE_ICQ = 3;
|
||||
const CHAT_TYPE_XMPP = 4;
|
||||
const CHAT_TYPE_MSN = 5;
|
||||
const CHAT_TYPE_SKYPE = 6;
|
||||
const CHAT_TYPE_YAHOO = 7;
|
||||
const CHAT_TYPE_QQ = 8;
|
||||
const CHAT_TYPE_WECHAT = 10;
|
||||
const CHAT_TYPE_OTHER = 9;
|
||||
// const CHAT_TYPE_AIM = 1;
|
||||
// const CHAT_TYPE_GTALK = 2;
|
||||
// const CHAT_TYPE_ICQ = 3;
|
||||
// const CHAT_TYPE_XMPP = 4;
|
||||
// const CHAT_TYPE_MSN = 5;
|
||||
// const CHAT_TYPE_SKYPE = 6;
|
||||
// const CHAT_TYPE_YAHOO = 7;
|
||||
// const CHAT_TYPE_QQ = 8;
|
||||
// const CHAT_TYPE_WECHAT = 10;
|
||||
// const CHAT_TYPE_OTHER = 9;
|
||||
|
||||
/**
|
||||
* For API.
|
||||
*/
|
||||
public static $chat_types = [
|
||||
self::CHAT_TYPE_AIM => 'aim',
|
||||
self::CHAT_TYPE_GTALK => 'gtalk',
|
||||
self::CHAT_TYPE_ICQ => 'icq',
|
||||
self::CHAT_TYPE_XMPP => 'xmpp',
|
||||
self::CHAT_TYPE_MSN => 'msn',
|
||||
self::CHAT_TYPE_SKYPE => 'skype',
|
||||
self::CHAT_TYPE_YAHOO => 'yahoo',
|
||||
self::CHAT_TYPE_QQ => 'qq',
|
||||
self::CHAT_TYPE_WECHAT => 'wechat', // Extra
|
||||
self::CHAT_TYPE_OTHER => 'other',
|
||||
];
|
||||
// public static $chat_types = [
|
||||
// self::CHAT_TYPE_AIM => 'aim',
|
||||
// self::CHAT_TYPE_GTALK => 'gtalk',
|
||||
// self::CHAT_TYPE_ICQ => 'icq',
|
||||
// self::CHAT_TYPE_XMPP => 'xmpp',
|
||||
// self::CHAT_TYPE_MSN => 'msn',
|
||||
// self::CHAT_TYPE_SKYPE => 'skype',
|
||||
// self::CHAT_TYPE_YAHOO => 'yahoo',
|
||||
// self::CHAT_TYPE_QQ => 'qq',
|
||||
// self::CHAT_TYPE_WECHAT => 'wechat', // Extra
|
||||
// self::CHAT_TYPE_OTHER => 'other',
|
||||
// ];
|
||||
|
||||
public static $chat_type_names = [
|
||||
self::CHAT_TYPE_AIM => 'AIM',
|
||||
self::CHAT_TYPE_GTALK => 'Google+',
|
||||
self::CHAT_TYPE_ICQ => 'ICQ',
|
||||
self::CHAT_TYPE_XMPP => 'XMPP',
|
||||
self::CHAT_TYPE_MSN => 'MSN',
|
||||
self::CHAT_TYPE_SKYPE => 'Skype',
|
||||
self::CHAT_TYPE_YAHOO => 'Yahoo',
|
||||
self::CHAT_TYPE_QQ => 'QQ',
|
||||
self::CHAT_TYPE_WECHAT => 'WeChat', // Extra
|
||||
self::CHAT_TYPE_OTHER => 'Other',
|
||||
];
|
||||
// public static $chat_type_names = [
|
||||
// self::CHAT_TYPE_AIM => 'AIM',
|
||||
// self::CHAT_TYPE_GTALK => 'Google+',
|
||||
// self::CHAT_TYPE_ICQ => 'ICQ',
|
||||
// self::CHAT_TYPE_XMPP => 'XMPP',
|
||||
// self::CHAT_TYPE_MSN => 'MSN',
|
||||
// self::CHAT_TYPE_SKYPE => 'Skype',
|
||||
// self::CHAT_TYPE_YAHOO => 'Yahoo',
|
||||
// self::CHAT_TYPE_QQ => 'QQ',
|
||||
// self::CHAT_TYPE_WECHAT => 'WeChat', // Extra
|
||||
// self::CHAT_TYPE_OTHER => 'Other',
|
||||
// ];
|
||||
|
||||
/**
|
||||
* Social types.
|
||||
*/
|
||||
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;
|
||||
@@ -138,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',
|
||||
@@ -154,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',
|
||||
@@ -167,6 +177,13 @@ class Customer extends Model
|
||||
self::SOCIAL_TYPE_OTHER => 'Other',
|
||||
];
|
||||
|
||||
/**
|
||||
* Search filters.
|
||||
*/
|
||||
public static $search_filters = [
|
||||
'mailbox',
|
||||
];
|
||||
|
||||
/**
|
||||
* Countries list.
|
||||
*/
|
||||
@@ -423,6 +440,10 @@ class Customer extends Model
|
||||
'ZW' => 'Zimbabwe',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Attributes which are not fillable using fill() method.
|
||||
*/
|
||||
@@ -433,12 +454,12 @@ class Customer extends Model
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country'];
|
||||
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'age', 'gender', 'notes', 'channel', 'channel_id', 'social_profiles'];
|
||||
|
||||
/**
|
||||
* Fields stored as JSON.
|
||||
*/
|
||||
protected $json_fields = ['phones', 'websites', 'social_profiles', 'chats'];
|
||||
protected $json_fields = ['phones', 'websites', 'social_profiles'];
|
||||
|
||||
/**
|
||||
* Get customer emails.
|
||||
@@ -472,6 +493,14 @@ class Customer extends Model
|
||||
return optional($this->emails_cached()->first())->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main email.
|
||||
*/
|
||||
public static function getMainEmailStatic($customer_id)
|
||||
{
|
||||
return Email::select('email')->where('customer_id', $customer_id)->pluck('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer full name.
|
||||
*
|
||||
@@ -598,10 +627,17 @@ class Customer extends Model
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPhones()
|
||||
public function getPhones($dummy_if_empty = false)
|
||||
{
|
||||
if ($this->phones) {
|
||||
return 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' => '',
|
||||
]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -639,9 +675,13 @@ class Customer extends Model
|
||||
public static function formatPhones(array $phones_array)
|
||||
{
|
||||
$phones = [];
|
||||
|
||||
foreach ($phones_array as $phone) {
|
||||
if (is_array($phone)) {
|
||||
if (!empty($phone['value']) && !empty($phone['type']) && in_array($phone['type'], array_keys(self::$phone_types))) {
|
||||
if (!empty($phone['value'])) {
|
||||
if (empty($phone['type']) || !in_array($phone['type'], array_keys(self::$phone_types))) {
|
||||
$phone['type'] = self::PHONE_TYPE_WORK;
|
||||
}
|
||||
$phones[] = [
|
||||
'value' => (string) $phone['value'],
|
||||
'type' => (int) $phone['type'],
|
||||
@@ -663,10 +703,26 @@ class Customer extends Model
|
||||
*/
|
||||
public function addPhone($phone, $type = self::PHONE_TYPE_WORK)
|
||||
{
|
||||
$this->setPhones(array_merge(
|
||||
$this->getPhones(),
|
||||
[['value' => $phone, 'type' => $type]]
|
||||
));
|
||||
if (is_string($phone)) {
|
||||
$this->setPhones(array_merge(
|
||||
$this->getPhones(),
|
||||
[['value' => $phone, 'type' => $type]]
|
||||
));
|
||||
} else {
|
||||
$this->setPhones(array_merge(
|
||||
$this->getPhones(),
|
||||
[$phone]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find customer by phone number.
|
||||
*/
|
||||
public static function findByPhone($phone)
|
||||
{
|
||||
$phone = trim($phone);
|
||||
return Customer::where('phones', 'LIKE', '%"'.$phone.'"%')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -674,10 +730,17 @@ class Customer extends Model
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSocialProfiles()
|
||||
public function getSocialProfiles($dummy_if_empty = false)
|
||||
{
|
||||
if ($this->social_profiles) {
|
||||
$social_profiles = json_decode($this->social_profiles ?? '', true);
|
||||
|
||||
if (is_array($social_profiles) && count($social_profiles)) {
|
||||
return json_decode($this->social_profiles, true);
|
||||
} elseif ($dummy_if_empty) {
|
||||
return [[
|
||||
'type' => '',
|
||||
'value' => '',
|
||||
]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
@@ -690,8 +753,9 @@ class Customer extends Model
|
||||
*/
|
||||
public function getWebsites($dummy_if_empty = false)
|
||||
{
|
||||
if ($this->websites) {
|
||||
return json_decode($this->websites, true);
|
||||
$websites = json_decode($this->websites ?? '', true);
|
||||
if (is_array($websites) && count($websites)) {
|
||||
return $websites;
|
||||
} elseif ($dummy_if_empty) {
|
||||
return [''];
|
||||
} else {
|
||||
@@ -710,6 +774,12 @@ class Customer extends Model
|
||||
foreach ($websites_array as $key => $value) {
|
||||
// FILTER_SANITIZE_URL cuts some symbols.
|
||||
//$value = filter_var((string) $value, FILTER_SANITIZE_URL);
|
||||
if (isset($value['value'])) {
|
||||
$value = $value['value'];
|
||||
}
|
||||
if (!$value || preg_match("/^http(s)?:?\/?\/?$/i", $value)) {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match("/http(s)?:\/\//i", $value)) {
|
||||
$value = 'http://'.$value;
|
||||
}
|
||||
@@ -723,7 +793,81 @@ class Customer extends Model
|
||||
*/
|
||||
public function addWebsite($website)
|
||||
{
|
||||
$this->setWebsites(array_merge($this->getWebsites(), [$value]));
|
||||
$websites = $this->getWebsites();
|
||||
if (isset($website['value'])) {
|
||||
$website = $website['value'];
|
||||
}
|
||||
array_push($websites, $website);
|
||||
$this->setWebsites($websites);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize social profiles.
|
||||
*
|
||||
* @param array $list [description]
|
||||
*
|
||||
* @return array [description]
|
||||
*/
|
||||
public static function formatSocialProfiles(array $list)
|
||||
{
|
||||
$social_profiles = [];
|
||||
foreach ($list as $social_profile) {
|
||||
if (is_array($social_profile)) {
|
||||
if (!empty($social_profile['value']) && !empty($social_profile['type'])) {
|
||||
|
||||
$type = null;
|
||||
|
||||
if (is_numeric($social_profile['type']) && in_array($social_profile['type'], array_keys(self::$social_types))) {
|
||||
$type = (int)$social_profile['type'];
|
||||
} else {
|
||||
// Find type.
|
||||
foreach (self::$social_types as $type_id => $type_name) {
|
||||
if ($type_name == strtolower($social_profile['type'])) {
|
||||
$type = $type_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$social_profiles[] = [
|
||||
'value' => (string) $social_profile['value'],
|
||||
'type' => $type,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$social_profiles[] = [
|
||||
'value' => (string) $social_profile,
|
||||
'type' => self::SOCIAL_TYPE_OTHER,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $social_profiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set social profiles as JSON.
|
||||
*
|
||||
* @param array $websites_array
|
||||
*/
|
||||
public function setSocialProfiles(array $sp_array)
|
||||
{
|
||||
$sp_array = self::formatSocialProfiles($sp_array);
|
||||
|
||||
// Remove dubplicates.
|
||||
$list = [];
|
||||
foreach ($sp_array as $i => $data) {
|
||||
if (in_array($data['value'], $list)) {
|
||||
unset($sp_array[$i]);
|
||||
} else {
|
||||
$list[] = $data['value'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->social_profiles = \Helper::jsonEncodeUtf8($sp_array);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -736,6 +880,8 @@ class Customer extends Model
|
||||
*/
|
||||
public static function create($email, $data = [])
|
||||
{
|
||||
$new = false;
|
||||
|
||||
$email = Email::sanitizeEmail($email);
|
||||
if (!$email) {
|
||||
return null;
|
||||
@@ -744,6 +890,12 @@ class Customer extends Model
|
||||
if ($email_obj) {
|
||||
$customer = $email_obj->customer;
|
||||
|
||||
// In case somehow the email has no customer.
|
||||
if (!$customer) {
|
||||
// Customer will be saved and connected to the email later.
|
||||
$customer = new self();
|
||||
}
|
||||
|
||||
// Update name if empty.
|
||||
/*if (empty($customer->first_name) && !empty($data['first_name'])) {
|
||||
$customer->first_name = $data['first_name'];
|
||||
@@ -756,6 +908,8 @@ class Customer extends Model
|
||||
$customer = new self();
|
||||
$email_obj = new Email();
|
||||
$email_obj->email = $email;
|
||||
|
||||
$new = true;
|
||||
}
|
||||
|
||||
// Set empty fields
|
||||
@@ -763,9 +917,31 @@ 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.
|
||||
|
||||
if ($new) {
|
||||
\Eventy::action('customer.created', $customer);
|
||||
}
|
||||
|
||||
return $customer;
|
||||
@@ -778,9 +954,24 @@ class Customer extends Model
|
||||
{
|
||||
$result = false;
|
||||
|
||||
// todo: photoUrl.
|
||||
if (isset($data['photo_url'])) {
|
||||
unset($data['photo_url']);
|
||||
}
|
||||
|
||||
if (!empty($data['background']) && empty($data['notes'])) {
|
||||
$data['notes'] = $data['background'];
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -793,23 +984,80 @@ class Customer extends Model
|
||||
}
|
||||
|
||||
// Set JSON values.
|
||||
if (!empty($data['phone'])) {
|
||||
$this->addPhone($data['phone']);
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
if (!in_array($key, $this->json_fields)) {
|
||||
if (!in_array($key, $this->json_fields) && $key != 'emails') {
|
||||
continue;
|
||||
}
|
||||
// todo: setChats, setSocialProfiles
|
||||
if ($key == 'phones') {
|
||||
foreach ($value as $phone_value) {
|
||||
$this->addPhone($phone_value);
|
||||
if (isset($value['value'])) {
|
||||
$this->addPhone($value);
|
||||
} else {
|
||||
$this->setPhones($value);
|
||||
// foreach ($value as $phone_value) {
|
||||
// $this->addPhone($phone_value);
|
||||
// }
|
||||
}
|
||||
$result = true;
|
||||
}
|
||||
if ($key == 'websites') {
|
||||
$this->addWebsite($value);
|
||||
if (is_array($value)) {
|
||||
$this->setWebsites($value);
|
||||
// foreach ($value as $website) {
|
||||
// $this->addWebsite($website);
|
||||
// }
|
||||
} else {
|
||||
$this->addWebsite($value);
|
||||
}
|
||||
$result = true;
|
||||
}
|
||||
if ($key == 'social_profiles') {
|
||||
$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.
|
||||
|
||||
\Eventy::action('customer.set_data', $this, $data, $replace_data);
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
@@ -818,14 +1066,18 @@ class Customer extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a customer, email is not required.
|
||||
* For phone conversations.
|
||||
*/
|
||||
public static function createWithoutEmail($data = [])
|
||||
{
|
||||
$customer = new self();
|
||||
$customer->fill($data);
|
||||
$customer->setData($data);
|
||||
|
||||
$customer->save();
|
||||
|
||||
\Eventy::action('customer.created', $customer);
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
@@ -839,6 +1091,16 @@ class Customer extends Model
|
||||
return route('customers.update', ['id'=>$this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get view customer URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function urlView()
|
||||
{
|
||||
return route('customers.conversations', ['id'=>$this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date according to customer's timezone.
|
||||
*
|
||||
@@ -852,11 +1114,6 @@ class Customer extends Model
|
||||
return $date->format($format);
|
||||
}
|
||||
|
||||
public function getPhotoUrl()
|
||||
{
|
||||
return asset('/img/default-avatar.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full representation of customer.
|
||||
*/
|
||||
@@ -911,14 +1168,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;
|
||||
@@ -952,4 +1214,193 @@ class Customer extends Model
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getPhotoUrl($default_if_empty = true)
|
||||
{
|
||||
if (!empty($this->photo_url) || !$default_if_empty) {
|
||||
if (!empty($this->photo_url)) {
|
||||
return self::getPhotoUrlByFileName($this->photo_url);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return \Eventy::filter('customer.default_avatar', '/img/default-avatar.png', $this);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getPhotoUrlByFileName($file_name)
|
||||
{
|
||||
return Storage::url(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize and save photo.
|
||||
*/
|
||||
public function savePhoto($real_path, $mime_type)
|
||||
{
|
||||
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, self::PHOTO_SIZE, self::PHOTO_SIZE);
|
||||
|
||||
if (!$resized_image) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file_name = md5(Hash::make($this->id)).'.jpg';
|
||||
$dest_path = Storage::path(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
|
||||
|
||||
$dest_dir = pathinfo($dest_path, PATHINFO_DIRNAME);
|
||||
if (!file_exists($dest_dir)) {
|
||||
\File::makeDirectory($dest_dir, 0755);
|
||||
}
|
||||
|
||||
// Remove current photo
|
||||
if ($this->photo_url) {
|
||||
Storage::delete(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$this->photo_url);
|
||||
}
|
||||
|
||||
imagejpeg($resized_image, $dest_path, self::PHOTO_QUALITY);
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user photo.
|
||||
*/
|
||||
public function removePhoto()
|
||||
{
|
||||
if ($this->photo_url) {
|
||||
Storage::delete(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$this->photo_url);
|
||||
}
|
||||
$this->photo_url = '';
|
||||
}
|
||||
|
||||
public function getCountryName()
|
||||
{
|
||||
if ($this->country && !empty(self::$countries[$this->country])) {
|
||||
return self::$countries[$this->country];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first and last name.
|
||||
*/
|
||||
public static function parseName($name)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
if (!$name) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$name_parts = explode(' ', $name, 2);
|
||||
$data['first_name'] = $name_parts[0];
|
||||
if (!empty($name_parts[1])) {
|
||||
$data['last_name'] = $name_parts[1];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function formatSocialProfile($sp)
|
||||
{
|
||||
if (empty($sp['type']) || !isset(self::$social_type_names[$sp['type']])) {
|
||||
$sp['type'] = self::SOCIAL_TYPE_OTHER;
|
||||
}
|
||||
|
||||
$sp['type_name'] = self::$social_type_names[$sp['type']];
|
||||
|
||||
$sp['value_url'] = $sp['value'];
|
||||
|
||||
if (!preg_match("/^https?:\/\//i", $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'] = '';
|
||||
}
|
||||
|
||||
return $sp;
|
||||
}
|
||||
|
||||
public function setPhotoFromRemoteFile($url)
|
||||
{
|
||||
$headers = get_headers($url);
|
||||
|
||||
if (!preg_match("/200/", $headers[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$image_data = file_get_contents($url);
|
||||
|
||||
if (!$image_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$temp_file = tempnam(sys_get_temp_dir(), 'photo');
|
||||
|
||||
\File::put($temp_file, $image_data);
|
||||
|
||||
$photo_url = $this->savePhoto($temp_file, \File::mimeType($temp_file));
|
||||
|
||||
if ($photo_url) {
|
||||
$this->photo_url = $photo_url;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getChannelName()
|
||||
{
|
||||
return \Eventy::filter('channel.name', '', $this->channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get thread meta value.
|
||||
*/
|
||||
public function getMeta($key, $default = null)
|
||||
{
|
||||
if (isset($this->meta[$key])) {
|
||||
return $this->meta[$key];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set thread meta value.
|
||||
*/
|
||||
public function setMeta($key, $value)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -69,4 +69,19 @@ class Email extends Model
|
||||
{
|
||||
return explode('@', $this->email)[0];
|
||||
}
|
||||
|
||||
public static function create($email, $customer_id, $type = self::TYPE_WORK)
|
||||
{
|
||||
try {
|
||||
$email_obj = new Email();
|
||||
$email_obj->email = $email;
|
||||
$email_obj->type = array_key_exists($type, self::$types) ? $type : self::TYPE_WORK;
|
||||
$email_obj->customer_id = $customer_id;
|
||||
$email_obj->save();
|
||||
|
||||
return $email_obj;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,11 @@ class RealtimeMailboxNewThread implements ShouldBroadcastNow
|
||||
return [];
|
||||
}
|
||||
|
||||
$folder = Folder::find(Conversation::getFolderParam());
|
||||
$folder = null;
|
||||
$foler_id = Conversation::getFolderParam();
|
||||
if ($foler_id) {
|
||||
$folder = Folder::find($foler_id);
|
||||
}
|
||||
// Just in case.
|
||||
if (!$folder) {
|
||||
$folder = new Folder();
|
||||
|
||||
121
app/Folder.php
121
app/Folder.php
@@ -23,12 +23,12 @@ class Folder extends Model
|
||||
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']];
|
||||
@@ -169,6 +169,30 @@ class Folder extends Model
|
||||
case self::TYPE_DELETED:
|
||||
$order_by = [['user_updated_at' => 'desc']];
|
||||
break;
|
||||
|
||||
default:
|
||||
$order_by = \Eventy::filter('folder.conversations_order_by', $order_by, $this->type);
|
||||
break;
|
||||
}
|
||||
|
||||
// Process columns sorting.
|
||||
$sorting = Conversation::getConvTableSorting();
|
||||
if ($sorting['sort_by'] == 'date') {
|
||||
if ($sorting['order'] != 'desc') {
|
||||
foreach ($order_by as $block_i => $block) {
|
||||
foreach ($block as $field => $order) {
|
||||
if ($field == 'status') {
|
||||
unset($order_by[$block_i][$field]);
|
||||
} else {
|
||||
if ($order == 'desc') {
|
||||
$order_by[$block_i][$field] = 'asc';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$order_by = [[$sorting['sort_by'] => $sorting['order']]];
|
||||
}
|
||||
|
||||
return $order_by;
|
||||
@@ -205,8 +229,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)
|
||||
@@ -248,7 +272,14 @@ class Folder extends Model
|
||||
*/
|
||||
public function getCount($folders = [])
|
||||
{
|
||||
if (\Eventy::filter('folder.counter', self::COUNTER_ACTIVE, $this, $folders) == self::COUNTER_TOTAL || $this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
|
||||
$counter = \Eventy::filter('folder.counter', self::COUNTER_ACTIVE, $this, $folders);
|
||||
|
||||
$count = \Eventy::filter('folder.count', false, $this, $counter, $folders);
|
||||
if ($count !== false) {
|
||||
return $count;
|
||||
}
|
||||
|
||||
if ($counter == self::COUNTER_TOTAL || $this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
|
||||
return $this->total_count;
|
||||
} else {
|
||||
return $this->getActiveCount($folders);
|
||||
@@ -262,10 +293,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) {
|
||||
@@ -339,4 +374,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class LoginController extends Controller
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
protected $redirectTo = '/home';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
|
||||
572
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
572
app/Http/Controllers/ConversationsController.php
Normal file → Executable file
@@ -44,12 +44,20 @@ class ConversationsController extends Controller
|
||||
public function view(Request $request, $id)
|
||||
{
|
||||
$conversation = Conversation::findOrFail($id);
|
||||
$this->authorize('view', $conversation);
|
||||
$this->authorize('viewCached', $conversation);
|
||||
|
||||
$mailbox = $conversation->mailbox;
|
||||
$customer = $conversation->customer_cached;
|
||||
$user = auth()->user();
|
||||
|
||||
// To let other parts of the app easily access.
|
||||
\Helper::setGlobalEntity('conversation', $conversation);
|
||||
\Helper::setGlobalEntity('mailbox', $mailbox);
|
||||
|
||||
if ($user->isAdmin()) {
|
||||
$mailbox->fetchUserSettings($user->id);
|
||||
}
|
||||
|
||||
// Mark notifications as read
|
||||
if (!empty($request->mark_as_read)) {
|
||||
$mark_read_result = $user->unreadNotifications()->where('id', $request->mark_as_read)->update(['read_at' => now()]);
|
||||
@@ -105,7 +113,8 @@ class ConversationsController extends Controller
|
||||
return redirect()->away($conversation->url($folder->id));
|
||||
}
|
||||
|
||||
$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
|
||||
//$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
|
||||
$after_send = $user->mailboxSettings($conversation->mailbox_id)->after_send;
|
||||
|
||||
// Detect customers and emails to which user can reply
|
||||
$to_customers = [];
|
||||
@@ -170,7 +179,8 @@ class ConversationsController extends Controller
|
||||
$prev_conversations = $mailbox->conversations()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('id', '<>', $conversation->id)
|
||||
->where('status', '!=', Thread::STATUS_SPAM)
|
||||
->where('status', '!=', Conversation::STATUS_SPAM)
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
//->limit(self::PREV_CONVERSATIONS_LIMIT)
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(self::PREV_CONVERSATIONS_LIMIT);
|
||||
@@ -238,7 +248,7 @@ class ConversationsController extends Controller
|
||||
}
|
||||
// Show replying first.
|
||||
usort($viewers, function($a, $b) {
|
||||
return $a['replying'] < $b['replying'];
|
||||
return $b['replying'] <=> $a['replying'];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -258,7 +268,7 @@ class ConversationsController extends Controller
|
||||
'to_customers' => $to_customers,
|
||||
'prev_conversations' => $prev_conversations,
|
||||
'cc' => $conversation->getCcArray($exclude_array),
|
||||
'bcc' => $conversation->getBccArray($exclude_array),
|
||||
'bcc' => [], //$conversation->getBccArray($exclude_array),
|
||||
// Data for creating a phone conversation.
|
||||
'name' => $name,
|
||||
'phone' => $phone,
|
||||
@@ -282,6 +292,7 @@ class ConversationsController extends Controller
|
||||
|
||||
$folder = $mailbox->folders()->where('type', Folder::TYPE_DRAFTS)->first();
|
||||
|
||||
// todo: use $user->mailboxSettings()
|
||||
$after_send = $mailbox->getUserSettings(auth()->user()->id)->after_send;
|
||||
|
||||
// Create conversation from thread
|
||||
@@ -308,6 +319,13 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$to = [];
|
||||
|
||||
$prefill_to = \App\Email::sanitizeEmail($request->get('to'));
|
||||
if ($prefill_to) {
|
||||
$to = [$prefill_to => $prefill_to];
|
||||
}
|
||||
|
||||
return view('conversations/create', [
|
||||
'conversation' => $conversation,
|
||||
'thread' => $thread,
|
||||
@@ -315,7 +333,7 @@ class ConversationsController extends Controller
|
||||
'folder' => $folder,
|
||||
'folders' => $mailbox->getAssesibleFolders(),
|
||||
'after_send' => $after_send,
|
||||
'to' => [],
|
||||
'to' => $to,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -479,6 +497,7 @@ class ConversationsController extends Controller
|
||||
$is_note = true;
|
||||
}
|
||||
|
||||
// Conversation type.
|
||||
$type = Conversation::TYPE_EMAIL;
|
||||
if (!empty($request->type)) {
|
||||
$type = (int)$request->type;
|
||||
@@ -606,12 +625,20 @@ class ConversationsController extends Controller
|
||||
if ((int) $request->status != (int) $conversation->status) {
|
||||
$status_changed = true;
|
||||
}
|
||||
if (!empty($request->subject)) {
|
||||
$conversation->subject = $request->subject;
|
||||
}
|
||||
// When switching from regular message to phone and message sent
|
||||
// without saving a draft type need to be saved here.
|
||||
if ($conversation->type == Conversation::TYPE_EMAIL && $type == Conversation::TYPE_PHONE) {
|
||||
$conversation->type = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($attachments_info['has_attachments']) {
|
||||
$conversation->has_attachments = true;
|
||||
}
|
||||
|
||||
|
||||
// Customer can be empty in existing conversation if this is a draft.
|
||||
$customer_email = '';
|
||||
$customer = null;
|
||||
@@ -622,6 +649,9 @@ class ConversationsController extends Controller
|
||||
|
||||
$customer_email = $phone_customer_data['customer_email'];
|
||||
$customer = $phone_customer_data['customer'];
|
||||
if (!$conversation->customer_id) {
|
||||
$conversation->customer_id = $customer->id;
|
||||
}
|
||||
} else {
|
||||
// Email.
|
||||
if (!empty($to_array)) {
|
||||
@@ -641,6 +671,11 @@ class ConversationsController extends Controller
|
||||
$prev_status = $conversation->status;
|
||||
|
||||
$conversation->status = $request->status;
|
||||
if ($prev_status != $conversation->status && $conversation->status == Conversation::STATUS_CLOSED) {
|
||||
$conversation->closed_by_user_id = $user->id;
|
||||
$conversation->closed_at = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
// We need to set state, as it may have been a draft
|
||||
$conversation->state = Conversation::STATE_PUBLISHED;
|
||||
// Set assignee
|
||||
@@ -660,7 +695,7 @@ class ConversationsController extends Controller
|
||||
// List of emails.
|
||||
$to_list = [];
|
||||
if ($is_forward) {
|
||||
$to = $request->to_email;
|
||||
$to = $request->to_email[0];
|
||||
} else {
|
||||
if (!empty($request->to)) {
|
||||
// When creating a new conversation, to is a list of emails.
|
||||
@@ -686,6 +721,10 @@ class ConversationsController extends Controller
|
||||
$conversation->last_reply_from = Conversation::PERSON_USER;
|
||||
$conversation->user_updated_at = $now;
|
||||
}
|
||||
if ($conversation->isPhone() && $is_note) {
|
||||
$conversation->last_reply_at = $now;
|
||||
$conversation->last_reply_from = Conversation::PERSON_USER;
|
||||
}
|
||||
$conversation->updateFolder();
|
||||
if ($from_draft) {
|
||||
// Increment number of replies in conversation
|
||||
@@ -701,6 +740,8 @@ class ConversationsController extends Controller
|
||||
}
|
||||
|
||||
// Fire events
|
||||
\Eventy::action('conversation.send_reply_save', $conversation, $request);
|
||||
|
||||
if (!$new) {
|
||||
if ($status_changed) {
|
||||
event(new ConversationStatusChanged($conversation));
|
||||
@@ -756,51 +797,83 @@ class ConversationsController extends Controller
|
||||
if (!empty($request->saved_reply_id)) {
|
||||
$thread->saved_reply_id = $request->saved_reply_id;
|
||||
}
|
||||
if ($is_forward) {
|
||||
// Create forwarded conversation.
|
||||
$forwarded_conversation = $conversation->replicate();
|
||||
$forwarded_conversation->type = Conversation::TYPE_EMAIL;
|
||||
$forwarded_conversation->setPreview($thread->body);
|
||||
$forwarded_conversation->created_by_user_id = auth()->user()->id;
|
||||
$forwarded_conversation->source_via = Conversation::PERSON_USER;
|
||||
$forwarded_conversation->source_type = Conversation::SOURCE_TYPE_WEB;
|
||||
$forwarded_conversation->threads_count = 0; // Counter will be incremented in ThreadObserver.
|
||||
$forwarded_customer = Customer::create($customer_email);
|
||||
$forwarded_conversation->customer_id = $forwarded_customer->id;
|
||||
$forwarded_conversation->customer_email = $customer_email;
|
||||
$forwarded_conversation->subject = 'Fwd: '.$forwarded_conversation->subject;
|
||||
$forwarded_conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
|
||||
$forwarded_conversation->setBcc($request->bcc);
|
||||
$forwarded_conversation->last_reply_at = $now;
|
||||
$forwarded_conversation->last_reply_from = Conversation::PERSON_USER;
|
||||
$forwarded_conversation->user_updated_at = $now;
|
||||
if ($attachments_info['has_attachments']) {
|
||||
$forwarded_conversation->has_attachments = true;
|
||||
}
|
||||
$forwarded_conversation->updateFolder();
|
||||
$forwarded_conversation->save();
|
||||
|
||||
$forwarded_conversations = [];
|
||||
$forwarded_threads = [];
|
||||
|
||||
$forwarded_thread = $thread->replicate();
|
||||
if ($is_forward) {
|
||||
// Create forwarded conversations.
|
||||
foreach ($to_array as $recipient_email) {
|
||||
$forwarded_conversation = $conversation->replicate();
|
||||
$forwarded_conversation->type = Conversation::TYPE_EMAIL;
|
||||
$forwarded_conversation->setPreview($thread->body);
|
||||
$forwarded_conversation->created_by_user_id = auth()->user()->id;
|
||||
$forwarded_conversation->source_via = Conversation::PERSON_USER;
|
||||
$forwarded_conversation->source_type = Conversation::SOURCE_TYPE_WEB;
|
||||
$forwarded_conversation->threads_count = 0; // Counter will be incremented in ThreadObserver.
|
||||
$forwarded_customer = Customer::create($recipient_email);
|
||||
$forwarded_conversation->customer_id = $forwarded_customer->id;
|
||||
// Reload customer object, otherwise it stores previous customer.
|
||||
$forwarded_conversation->load('customer');
|
||||
$forwarded_conversation->customer_email = $recipient_email;
|
||||
$forwarded_conversation->subject = 'Fwd: '.$forwarded_conversation->subject;
|
||||
//$forwarded_conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
|
||||
$forwarded_conversation->setCc(Conversation::sanitizeEmails($request->cc));
|
||||
$forwarded_conversation->setBcc($request->bcc);
|
||||
$forwarded_conversation->last_reply_at = $now;
|
||||
$forwarded_conversation->last_reply_from = Conversation::PERSON_USER;
|
||||
$forwarded_conversation->user_updated_at = $now;
|
||||
if ($attachments_info['has_attachments']) {
|
||||
$forwarded_conversation->has_attachments = true;
|
||||
}
|
||||
$forwarded_conversation->updateFolder();
|
||||
$forwarded_conversation->save();
|
||||
|
||||
$forwarded_thread = $thread->replicate();
|
||||
|
||||
$forwarded_conversations[] = $forwarded_conversation;
|
||||
$forwarded_threads[] = $forwarded_thread;
|
||||
}
|
||||
|
||||
// Set forwarding meta data.
|
||||
// todo: store array of numbers and IDs.
|
||||
$thread->subtype = Thread::SUBTYPE_FORWARD;
|
||||
$thread->setMeta('forward_child_conversation_number', $forwarded_conversation->number);
|
||||
$thread->setMeta('forward_child_conversation_id', $forwarded_conversation->id);
|
||||
}
|
||||
|
||||
// Conversation history.
|
||||
if (!empty($request->conv_history)) {
|
||||
if ($request->conv_history != 'global') {
|
||||
if ($is_forward && !empty($forwarded_threads)) {
|
||||
foreach ($forwarded_threads as $forwarded_thread) {
|
||||
$forwarded_thread->setMeta(Thread::META_CONVERSATION_HISTORY, $request->conv_history);
|
||||
}
|
||||
} else {
|
||||
$thread->setMeta(Thread::META_CONVERSATION_HISTORY, $request->conv_history);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('thread.before_save_from_request', $thread, $request);
|
||||
$thread->save();
|
||||
|
||||
// Save forwarded thread.
|
||||
if ($is_forward) {
|
||||
$forwarded_thread->conversation_id = $forwarded_conversation->id;
|
||||
$forwarded_thread->type = Thread::TYPE_MESSAGE;
|
||||
$forwarded_thread->subtype = null;
|
||||
if ($attachments_info['has_attachments']) {
|
||||
$forwarded_thread->has_attachments = true;
|
||||
foreach ($forwarded_conversations as $i => $forwarded_conversation) {
|
||||
$forwarded_thread = $forwarded_threads[$i];
|
||||
|
||||
$forwarded_thread->conversation_id = $forwarded_conversation->id;
|
||||
$forwarded_thread->type = Thread::TYPE_MESSAGE;
|
||||
$forwarded_thread->subtype = null;
|
||||
if ($attachments_info['has_attachments']) {
|
||||
$forwarded_thread->has_attachments = true;
|
||||
}
|
||||
$forwarded_thread->setMeta('forward_parent_conversation_number', $conversation->number);
|
||||
$forwarded_thread->setMeta('forward_parent_conversation_id', $conversation->id);
|
||||
$forwarded_thread->setMeta('forward_parent_thread_id', $thread->id);
|
||||
$forwarded_thread->save();
|
||||
}
|
||||
$forwarded_thread->setMeta('forward_parent_conversation_number', $conversation->number);
|
||||
$forwarded_thread->setMeta('forward_parent_conversation_id', $conversation->id);
|
||||
$forwarded_thread->setMeta('forward_parent_thread_id', $thread->id);
|
||||
$forwarded_thread->save();
|
||||
}
|
||||
|
||||
// If thread has been created from draft, remove the draft
|
||||
@@ -824,11 +897,22 @@ class ConversationsController extends Controller
|
||||
// Set thread_id for uploaded attachments
|
||||
if ($attachments_info['attachments']) {
|
||||
if ($is_forward) {
|
||||
$attachment_thread_id = $forwarded_thread->id;
|
||||
// Copy attachments for each thread.
|
||||
if (count($forwarded_threads) > 1) {
|
||||
$attachments = Attachment::whereIn('id', $attachments_info['attachments'])->get();
|
||||
}
|
||||
foreach ($forwarded_threads as $i => $forwarded_thread) {
|
||||
if ($i == 0) {
|
||||
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $forwarded_thread->id]);
|
||||
} else {
|
||||
foreach ($attachments as $attachment) {
|
||||
$attachment->duplicate($forwarded_thread->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$attachment_thread_id = $thread->id;
|
||||
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $thread->id]);
|
||||
}
|
||||
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $attachment_thread_id]);
|
||||
}
|
||||
|
||||
// When user creates a new conversation it may be saved as draft first.
|
||||
@@ -842,11 +926,15 @@ class ConversationsController extends Controller
|
||||
// Forward.
|
||||
// Notifications to users not sent.
|
||||
event(new UserAddedNote($conversation, $thread));
|
||||
// To send email with forwarded conversation.
|
||||
event(new UserReplied($forwarded_conversation, $forwarded_thread));
|
||||
\Eventy::action('conversation.user_forwarded_can_undo', $conversation, $thread, $forwarded_conversation, $forwarded_thread);
|
||||
// After Conversation::UNDO_TIMOUT period trigger final event.
|
||||
\Helper::backgroundAction('conversation.user_forwarded', [$conversation, $thread, $forwarded_conversation, $forwarded_thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
foreach ($forwarded_conversations as $i => $forwarded_conversation) {
|
||||
$forwarded_thread = $forwarded_threads[$i];
|
||||
|
||||
// To send email with forwarded conversation.
|
||||
event(new UserReplied($forwarded_conversation, $forwarded_thread));
|
||||
\Eventy::action('conversation.user_forwarded_can_undo', $conversation, $thread, $forwarded_conversation, $forwarded_thread);
|
||||
// After Conversation::UNDO_TIMOUT period trigger final event.
|
||||
\Helper::backgroundAction('conversation.user_forwarded', [$conversation, $thread, $forwarded_conversation, $forwarded_thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
}
|
||||
} elseif ($is_note) {
|
||||
// Note.
|
||||
event(new UserAddedNote($conversation, $thread));
|
||||
@@ -887,14 +975,26 @@ class ConversationsController extends Controller
|
||||
// Save conversation.
|
||||
$conversation_copy->threads_count = 0;
|
||||
$conversation_copy->customer_id = $customer_tmp->id;
|
||||
// Reload customer, otherwise all recipients will have the same name.
|
||||
$conversation_copy->load('customer');
|
||||
$conversation_copy->customer_email = $customer_email;
|
||||
$conversation_copy->has_attachments = $conversation->has_attachments;
|
||||
$conversation_copy->push();
|
||||
|
||||
$thread_copy->conversation_id = $conversation_copy->id;
|
||||
$thread_copy->customer_id = $customer_tmp->id;
|
||||
$thread_copy->has_attachments = $conversation->has_attachments;
|
||||
$thread_copy->setTo($customer_email);
|
||||
$thread_copy->push();
|
||||
|
||||
// Copy attachments.
|
||||
if (!empty($attachments_info['attachments'])) {
|
||||
$attachments = Attachment::whereIn('id', $attachments_info['attachments'])->get();
|
||||
foreach ($attachments as $attachment) {
|
||||
$attachment->duplicate($thread_copy->id);
|
||||
}
|
||||
}
|
||||
|
||||
// Events.
|
||||
// todo: allow to undo all emails
|
||||
event(new UserCreatedConversation($conversation_copy, $thread_copy));
|
||||
@@ -910,34 +1010,32 @@ class ConversationsController extends Controller
|
||||
$show_view_link = false;
|
||||
}
|
||||
|
||||
$flash_vars = ['%tag_start%' => '<strong>', '%tag_end%' => '</strong>', '%view_start%' => ' <a href="'.$conversation->url().'">', '%a_end%' => '</a> ', '%undo_start%' => ' <a href="'.route('conversations.undo', ['thread_id' => $thread->id]).'" class="text-danger">'];
|
||||
|
||||
if ($is_phone) {
|
||||
$flash_type = 'warning';
|
||||
if ($show_view_link) {
|
||||
$flash_text = ':%tag_start%Conversation created:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%';
|
||||
$flash_text = __(':%tag_start%Conversation created:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%', $flash_vars);
|
||||
} else {
|
||||
$flash_text = '<strong>'.__('Conversation created').'</strong>';
|
||||
}
|
||||
} elseif ($is_note) {
|
||||
$flash_type = 'warning';
|
||||
if ($show_view_link) {
|
||||
$flash_text = ':%tag_start%Note added:%tag_end% :%view_start%View:%a_end%';
|
||||
$flash_text = __(':%tag_start%Note added:%tag_end% :%view_start%View:%a_end%', $flash_vars);
|
||||
} else {
|
||||
$flash_text = '<strong>'.__('Note added').'</strong>';
|
||||
}
|
||||
} else {
|
||||
$flash_type = 'success';
|
||||
if ($show_view_link) {
|
||||
$flash_text = ':%tag_start%Email Sent:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%';
|
||||
$flash_text = __(':%tag_start%Email Sent:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%', $flash_vars);
|
||||
} else {
|
||||
$flash_text = ':%tag_start%Email Sent:%tag_end% :%undo_start%Undo:%a_end%';
|
||||
$flash_text = __(':%tag_start%Email Sent:%tag_end% :%undo_start%Undo:%a_end%', $flash_vars);
|
||||
}
|
||||
}
|
||||
$flash_message = __(
|
||||
$flash_text,
|
||||
['%tag_start%' => '<strong>', '%tag_end%' => '</strong>', '%view_start%' => ' <a href="'.$conversation->url().'">', '%a_end%' => '</a> ', '%undo_start%' => ' <a href="'.route('conversations.undo', ['thread_id' => $thread->id]).'" class="text-danger">']
|
||||
);
|
||||
|
||||
\Session::flash('flash_'.$flash_type.'_floating', $flash_message);
|
||||
\Session::flash('flash_'.$flash_type.'_floating', $flash_text);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1257,8 +1355,8 @@ class ConversationsController extends Controller
|
||||
$response['data'] = [
|
||||
'thread_id' => $thread->id,
|
||||
'to' => $thread->getToFirst(),
|
||||
'cc' => $thread->getCcString(),
|
||||
'bcc' => $thread->getBccString(),
|
||||
'cc' => $thread->getCcArray(),
|
||||
'bcc' => $thread->getBccArray(),
|
||||
'body' => $thread->body,
|
||||
'is_forward' => (int)$thread->isForward(),
|
||||
'attachments' => $attachments,
|
||||
@@ -1267,7 +1365,8 @@ class ConversationsController extends Controller
|
||||
}
|
||||
break;
|
||||
|
||||
// Load attachments from all threads in conversation.
|
||||
// Load attachments from all threads in conversation
|
||||
// when forwarding or creating a new conversation.
|
||||
case 'load_attachments':
|
||||
$conversation = Conversation::find($request->conversation_id);
|
||||
if (!$conversation) {
|
||||
@@ -1286,11 +1385,17 @@ class ConversationsController extends Controller
|
||||
foreach ($conversation->threads as $thread) {
|
||||
if ($thread->has_attachments) {
|
||||
foreach ($thread->attachments as $attachment) {
|
||||
if ($request->is_forwarding == 'true') {
|
||||
$attachment_copy = $attachment->duplicate($thread->id);
|
||||
} else {
|
||||
$attachment_copy = $attachment;
|
||||
}
|
||||
|
||||
$attachments[] = [
|
||||
'id' => $attachment->id,
|
||||
'name' => $attachment->file_name,
|
||||
'size' => $attachment->size,
|
||||
'url' => $attachment->url(),
|
||||
'id' => $attachment_copy->id,
|
||||
'name' => $attachment_copy->file_name,
|
||||
'size' => $attachment_copy->size,
|
||||
'url' => $attachment_copy->url(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1313,14 +1418,14 @@ class ConversationsController extends Controller
|
||||
$response['msg'] .= __('Action not authorized');
|
||||
}
|
||||
if (!$response['msg']) {
|
||||
$mailbox_user = $user->mailboxes()->where('mailbox_id', $request->mailbox_id)->first();
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $request->mailbox_id)->first();
|
||||
if (!$mailbox_user) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$user->mailboxes()->attach($request->mailbox_id);
|
||||
// $mailbox_user = new MailboxUser();
|
||||
// $mailbox_user->mailbox_id = $mailbox->id;
|
||||
// $mailbox_user->user_id = $user->id;
|
||||
$mailbox_user = $user->mailboxes()->where('mailbox_id', $request->mailbox_id)->first();
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $request->mailbox_id)->first();
|
||||
}
|
||||
$mailbox_user->settings->after_send = $request->value;
|
||||
$mailbox_user->settings->save();
|
||||
@@ -1371,9 +1476,9 @@ class ConversationsController extends Controller
|
||||
|
||||
if (!$response['msg']) {
|
||||
if ($request->sub_action == 'star') {
|
||||
$conversation->addToFolder(Folder::TYPE_STARRED);
|
||||
$conversation->addToFolder(Folder::TYPE_STARRED, $user->id);
|
||||
} else {
|
||||
$conversation->removeFromFolder(Folder::TYPE_STARRED);
|
||||
$conversation->removeFromFolder(Folder::TYPE_STARRED, $user->id);
|
||||
}
|
||||
Conversation::clearStarredByUserCache($user->id, $conversation->mailbox_id);
|
||||
$conversation->mailbox->updateFoldersCounters(Folder::TYPE_STARRED);
|
||||
@@ -1507,6 +1612,13 @@ class ConversationsController extends Controller
|
||||
$response['body'] = $thread->getCleanBody();
|
||||
|
||||
if (strip_tags($response['body'])) {
|
||||
|
||||
// Update the preview for the conversation if needed.
|
||||
$last_thread = $thread->conversation->getLastThread([Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE]);
|
||||
if ($last_thread && $last_thread->id == $thread->id) {
|
||||
$thread->conversation->setPreview($thread->body);
|
||||
$thread->conversation->save();
|
||||
}
|
||||
$thread->save();
|
||||
|
||||
$response['status'] = 'success';
|
||||
@@ -1528,7 +1640,7 @@ class ConversationsController extends Controller
|
||||
if (!$user->can('update', $conversation)) {
|
||||
continue;
|
||||
}
|
||||
if (!$conversation->mailbox->userHasAccess($new_user_id)) {
|
||||
if ((int) $new_user_id != -1 && !$conversation->mailbox->userHasAccess($new_user_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1683,14 +1795,56 @@ class ConversationsController extends Controller
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$prev_folder_id = Conversation::getFolderParam();
|
||||
$prev_mailbox_id = $conversation->mailbox_id;
|
||||
|
||||
$conversation->moveToMailbox($mailbox, $user);
|
||||
|
||||
// If user does not have access to the new mailbox,
|
||||
// redirect to the previous mailbox.
|
||||
if (!$mailbox->userHasAccess($user->id)) {
|
||||
if (!empty($prev_folder_id)) {
|
||||
$response['redirect_url'] = route('mailboxes.view.folder', ['id' => $prev_mailbox_id, 'folder_id' => $prev_folder_id]);
|
||||
} else {
|
||||
$response['redirect_url'] = route('mailboxes.view', ['id' => $prev_mailbox_id]);
|
||||
}
|
||||
}
|
||||
|
||||
$response['status'] = 'success';
|
||||
\Session::flash('flash_success_floating', __('Conversation moved'));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Merge conversations
|
||||
case 'conversation_merge':
|
||||
$conversation = Conversation::find($request->conversation_id);
|
||||
|
||||
if (!$conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
if (!$response['msg'] && !$user->can('view', $conversation)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
$merge_conversation = Conversation::find($request->merge_conversation_id);
|
||||
|
||||
if (!$merge_conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
if (!$response['msg'] && !$user->can('view', $merge_conversation)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$conversation->mergeConversations($merge_conversation, $user);
|
||||
|
||||
$response['status'] = 'success';
|
||||
\Session::flash('flash_success_floating', __('Conversations merged'));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Follow conversation
|
||||
case 'follow':
|
||||
case 'unfollow':
|
||||
@@ -1732,6 +1886,47 @@ class ConversationsController extends Controller
|
||||
|
||||
break;
|
||||
|
||||
case 'update_subject':
|
||||
$conversation = Conversation::find($request->conversation_id);
|
||||
|
||||
if (!$conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
$subject = $request->value ?? '';
|
||||
$subject = trim($subject);
|
||||
|
||||
if (!$response['msg'] && $subject) {
|
||||
$conversation->subject = $subject;
|
||||
$conversation->save();
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'merge_search':
|
||||
$conversation = Conversation::where('number', $request->number)->first();
|
||||
|
||||
if (!$conversation) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
if (!$response['msg'] && !$user->can('view', $conversation)) {
|
||||
$response['msg'] = __('Conversation not found');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$response['html'] = \View::make('conversations/partials/merge_search_result')->with([
|
||||
'conversation' => $conversation
|
||||
])->render();
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
@@ -1758,6 +1953,10 @@ class ConversationsController extends Controller
|
||||
return $this->ajaxHtmlChangeCustomer();
|
||||
case 'move_conv':
|
||||
return $this->ajaxHtmlMoveConv();
|
||||
case 'merge_conv':
|
||||
return $this->ajaxHtmlMergeConv();
|
||||
case 'default_redirect':
|
||||
return $this->ajaxHtmlDefaultRedirect();
|
||||
}
|
||||
|
||||
abort(404);
|
||||
@@ -1891,6 +2090,73 @@ class ConversationsController extends Controller
|
||||
'mailboxes' => $user->mailboxesCanView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge conversations.
|
||||
*/
|
||||
public function ajaxHtmlMergeConv()
|
||||
{
|
||||
$conversation_id = Input::get('conversation_id');
|
||||
if (!$conversation_id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$conversation = Conversation::find($conversation_id);
|
||||
if (!$conversation) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user->can('view', $conversation)) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
|
||||
$prev_conversations = [];
|
||||
|
||||
if ($conversation->customer_id) {
|
||||
$prev_conversations = $conversation->mailbox->conversations()
|
||||
->where('customer_id', $conversation->customer_id)
|
||||
->where('id', '<>', $conversation->id)
|
||||
->where('status', '!=', Conversation::STATUS_SPAM)
|
||||
->where('state', Conversation::STATE_PUBLISHED)
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(500);
|
||||
}
|
||||
|
||||
return view('conversations/ajax_html/merge_conv', [
|
||||
'conversation' => $conversation,
|
||||
'prev_conversations' => $prev_conversations,
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change default redirect for the mailbox.
|
||||
*/
|
||||
public function ajaxHtmlDefaultRedirect()
|
||||
{
|
||||
$mailbox_id = Input::get('mailbox_id');
|
||||
if (!$mailbox_id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$mailbox = Mailbox::find($mailbox_id);
|
||||
if (!$mailbox) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user->can('view', $mailbox)) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('conversations/ajax_html/default_redirect', [
|
||||
'after_send' => $user->mailboxSettings($mailbox_id)->after_send,
|
||||
'mailbox_id' => $mailbox_id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirect URL after performing an action.
|
||||
@@ -1905,6 +2171,7 @@ class ConversationsController extends Controller
|
||||
if (!empty($request->after_send)) {
|
||||
$after_send = $request->after_send;
|
||||
} else {
|
||||
// todo: use $user->mailboxSettings()
|
||||
$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
|
||||
}
|
||||
if (!empty($after_send)) {
|
||||
@@ -1914,7 +2181,11 @@ class ConversationsController extends Controller
|
||||
$redirect_url = $conversation->url();
|
||||
break;
|
||||
case MailboxUser::AFTER_SEND_FOLDER:
|
||||
$redirect_url = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $conversation->folder_id]);
|
||||
$folder_id = Conversation::getFolderParam();
|
||||
if (!$folder_id) {
|
||||
$folder_id = $conversation->folder_id;
|
||||
}
|
||||
$redirect_url = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
|
||||
break;
|
||||
case MailboxUser::AFTER_SEND_NEXT:
|
||||
$redirect_url = $conversation->urlNext(Conversation::getFolderParam());
|
||||
@@ -1983,20 +2254,12 @@ class ConversationsController extends Controller
|
||||
*/
|
||||
public function ajaxConversationsPagination(Request $request, $response, $user)
|
||||
{
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
//$mailbox = Mailbox::find($request->mailbox_id);
|
||||
$folder = null;
|
||||
$conversations = [];
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
}
|
||||
|
||||
if (!$response['msg'] && !$user->can('view', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$folder = Folder::find($request->folder_id);
|
||||
$folder = \Eventy::filter('conversations.ajax_pagination_folder', Folder::find($request->folder_id), $request, $response, $user);
|
||||
if (!$folder) {
|
||||
$response['msg'] = __('Folder not found');
|
||||
}
|
||||
@@ -2005,6 +2268,11 @@ class ConversationsController extends Controller
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
// We should not use mailbox_id from the request, as it can be changed.
|
||||
if (!$response['msg'] && !$user->can('view', $folder->mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
|
||||
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->page);
|
||||
@@ -2052,21 +2320,27 @@ class ConversationsController extends Controller
|
||||
session()->put('recent_search_queries', $recent_search_queries);
|
||||
}
|
||||
|
||||
$conversations = $this->searchQuery($request, $user, $q, $filters);
|
||||
$conversations = [];
|
||||
if (\Eventy::filter('search.is_needed', true, 'conversations')) {
|
||||
$conversations = $this->searchQuery($user, $q, $filters);
|
||||
}
|
||||
$customers = $this->searchCustomers($request, $user);
|
||||
|
||||
// Dummy folder
|
||||
$folder = $this->getSearchFolder($conversations);
|
||||
|
||||
// List of available filters.
|
||||
$filters_list = \Eventy::filter('search.filters_list', Conversation::$search_filters, $mode, $filters, $q);
|
||||
//$filters_list_all = \Eventy::filter('search.filters_list_all', array_merge(Conversation::$search_filters, Customer::$search_filters), $filters, $q);
|
||||
if ($mode == Conversation::SEARCH_MODE_CONV) {
|
||||
$filters_list = \Eventy::filter('search.filters_list', Conversation::$search_filters, $mode, $filters, $q);
|
||||
} else {
|
||||
$filters_list = \Eventy::filter('search.filters_list_customers', Customer::$search_filters, $mode, $filters, $q);
|
||||
}
|
||||
|
||||
$mailboxes = \Cache::remember('search_filter_mailboxes_'.$user->id, 5, function () use ($user) {
|
||||
return $user->mailboxesCanView();
|
||||
});
|
||||
$users = \Cache::remember('search_filter_users_'.$user->id, 5, function () use ($user, $mailboxes) {
|
||||
return $user->whichUsersCanView($mailboxes);
|
||||
return \Eventy::filter('search.assignees', $user->whichUsersCanView($mailboxes), $user, $mailboxes);
|
||||
});
|
||||
|
||||
return view('conversations/search', [
|
||||
@@ -2100,103 +2374,9 @@ class ConversationsController extends Controller
|
||||
/**
|
||||
* Search conversations.
|
||||
*/
|
||||
public function searchQuery($request, $user, $q, $filters)
|
||||
public function searchQuery($user, $q, $filters)
|
||||
{
|
||||
// Get IDs of mailboxes to which user has access
|
||||
$mailbox_ids = $user->mailboxesIdsCanView();
|
||||
|
||||
// Filters
|
||||
//$filters = $request->f ?? [];
|
||||
|
||||
// Search query
|
||||
//$q = $this->getSearchQuery($request);
|
||||
|
||||
// Like is case insensitive.
|
||||
$like = '%'.mb_strtolower($q).'%';
|
||||
|
||||
$query_conversations = Conversation::select('conversations.*')
|
||||
// https://github.com/laravel/framework/issues/21242
|
||||
// https://github.com/laravel/framework/pull/27675
|
||||
->groupby('conversations.id')
|
||||
->whereIn('conversations.mailbox_id', $mailbox_ids)
|
||||
->join('threads', function ($join) {
|
||||
$join->on('conversations.id', '=', 'threads.conversation_id');
|
||||
});
|
||||
if ($q) {
|
||||
$query_conversations->where(function ($query) use ($like, $filters, $q) {
|
||||
$query->where('conversations.subject', 'like', $like)
|
||||
->orWhere('conversations.customer_email', 'like', $like)
|
||||
->orWhere('conversations.number', 'like', $like)
|
||||
->orWhere('conversations.id', 'like', $like)
|
||||
->orWhere('threads.body', 'like', $like)
|
||||
->orWhere('threads.from', 'like', $like)
|
||||
->orWhere('threads.to', 'like', $like)
|
||||
->orWhere('threads.cc', 'like', $like)
|
||||
->orWhere('threads.bcc', 'like', $like);
|
||||
|
||||
$query = \Eventy::filter('search.conversations.or_where', $query, $filters, $q);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply search filters.
|
||||
if (!empty($filters['assigned'])) {
|
||||
$query_conversations->where('conversations.user_id', $filters['assigned']);
|
||||
}
|
||||
if (!empty($filters['customer'])) {
|
||||
$customer_id = $filters['customer'];
|
||||
$query_conversations->where(function ($query) use ($customer_id) {
|
||||
$query->where('conversations.customer_id', '=', $customer_id)
|
||||
->orWhere('threads.created_by_customer_id', '=', $customer_id);
|
||||
});
|
||||
}
|
||||
if (!empty($filters['mailbox'])) {
|
||||
$query_conversations->where('conversations.mailbox_id', '=', $filters['mailbox']);
|
||||
}
|
||||
if (!empty($filters['status'])) {
|
||||
if (count($filters['status']) == 1) {
|
||||
// = is faster than IN.
|
||||
$query_conversations->where('conversations.status', '=', $filters['status'][0]);
|
||||
} else {
|
||||
$query_conversations->whereIn('conversations.status', $filters['status']);
|
||||
}
|
||||
}
|
||||
if (!empty($filters['subject'])) {
|
||||
$query_conversations->where('conversations.subject', 'like', '%'.mb_strtolower($filters['subject']).'%');
|
||||
}
|
||||
if (!empty($filters['attachments'])) {
|
||||
$has_attachments = ($filters['attachments'] == 'yes' ? true : false);
|
||||
$query_conversations->where('conversations.has_attachments', '=', $has_attachments);
|
||||
}
|
||||
if (!empty($filters['type'])) {
|
||||
$query_conversations->where('conversations.has_attachments', '=', $filters['type']);
|
||||
}
|
||||
if (!empty($filters['body'])) {
|
||||
$query_conversations->where('threads.body', 'like', '%'.mb_strtolower($filters['body']).'%');
|
||||
}
|
||||
if (!empty($filters['number'])) {
|
||||
$query_conversations->where('conversations.number', '=', $filters['number']);
|
||||
}
|
||||
if (!empty($filters['following'])) {
|
||||
if ($filters['following'] == 'yes') {
|
||||
$query_conversations->join('followers', function ($join) {
|
||||
$join->on('followers.conversation_id', '=', 'conversations.id');
|
||||
$join->where('followers.user_id', auth()->user()->id);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!empty($filters['id'])) {
|
||||
$query_conversations->where('conversations.id', '=', $filters['id']);
|
||||
}
|
||||
if (!empty($filters['after'])) {
|
||||
$query_conversations->where('conversations.created_at', '>=', date('Y-m-d 00:00:00', strtotime($filters['after'])));
|
||||
}
|
||||
if (!empty($filters['before'])) {
|
||||
$query_conversations->where('conversations.created_at', '<=', date('Y-m-d 23:59:59', strtotime($filters['before'])));
|
||||
}
|
||||
|
||||
$query_conversations = \Eventy::filter('search.conversations.apply_filters', $query_conversations, $filters, $q);
|
||||
|
||||
$query_conversations->orderBy('conversations.last_reply_at');
|
||||
$query_conversations = Conversation::search($q, $filters, $user);
|
||||
|
||||
return $query_conversations->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
}
|
||||
@@ -2260,7 +2440,9 @@ class ConversationsController extends Controller
|
||||
// Like is case insensitive.
|
||||
$like = '%'.mb_strtolower($q).'%';
|
||||
|
||||
$query_customers = Customer::select(['customers.*', 'emails.email'])
|
||||
// We need to use aggregate function for email to avoid "Grouping error" error in PostgreSQL.
|
||||
$query_customers = Customer::select(['customers.*', \DB::raw('MAX(emails.email)')])
|
||||
->groupby('customers.id')
|
||||
->leftJoin('emails', function ($join) {
|
||||
$join->on('customers.id', '=', 'emails.customer_id');
|
||||
})
|
||||
@@ -2272,7 +2454,6 @@ class ConversationsController extends Controller
|
||||
->orWhere('customers.phones', 'like', $like)
|
||||
->orWhere('customers.websites', 'like', $like)
|
||||
->orWhere('customers.social_profiles', 'like', $like)
|
||||
->orWhere('customers.chats', 'like', $like)
|
||||
->orWhere('customers.address', 'like', $like)
|
||||
->orWhere('customers.city', 'like', $like)
|
||||
->orWhere('customers.state', 'like', $like)
|
||||
@@ -2281,6 +2462,14 @@ class ConversationsController extends Controller
|
||||
->orWhere('emails.email', 'like', $like);
|
||||
});
|
||||
|
||||
if (!empty($filters['mailbox'])) {
|
||||
$query_customers->join('conversations', function ($join) use ($filters) {
|
||||
$join->on('conversations.customer_id', '=', 'customers.id');
|
||||
//$join->on('conversations.mailbox_id', '=', $filters['mailbox']);
|
||||
});
|
||||
$query_customers->where('conversations.mailbox_id', '=', $filters['mailbox']);
|
||||
}
|
||||
|
||||
$query_customers = \Eventy::filter('search.customers.apply_filters', $query_customers, $filters, $q);
|
||||
|
||||
return $query_customers->paginate(50);
|
||||
@@ -2306,7 +2495,7 @@ class ConversationsController extends Controller
|
||||
{
|
||||
if (array_key_exists('q', $request->filter)) {
|
||||
// Search
|
||||
$conversations = $this->searchQuery($request, $user, $this->getSearchQuery($request), $this->getSearchFilters($request));
|
||||
$conversations = $this->searchQuery($user, $this->getSearchQuery($request), $this->getSearchFilters($request));
|
||||
} else {
|
||||
// Filters
|
||||
$conversations = $this->conversationsFilterQuery($request, $user);
|
||||
@@ -2316,6 +2505,7 @@ class ConversationsController extends Controller
|
||||
|
||||
$response['html'] = view('conversations/conversations_table', [
|
||||
'conversations' => $conversations,
|
||||
'params' => $request->params ?? [],
|
||||
])->render();
|
||||
|
||||
return $response;
|
||||
@@ -2447,9 +2637,9 @@ class ConversationsController extends Controller
|
||||
$customer = null;
|
||||
|
||||
// Check to prevent creating empty customers.
|
||||
if (trim($request->name) || trim($request->phone)) {
|
||||
$request->name = trim($request->name);
|
||||
$request->phone = trim($request->phone);
|
||||
if (trim($request->name ?? '') || trim($request->phone ?? '')) {
|
||||
$request->name = trim($request->name ?? '');
|
||||
$request->phone = trim($request->phone ?? '');
|
||||
|
||||
$name_parts = explode(' ', $request->name);
|
||||
$customer_data['first_name'] = $name_parts[0];
|
||||
@@ -2473,6 +2663,14 @@ class ConversationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find customer by phone.
|
||||
if (!$customer && $request->phone) {
|
||||
$customer = Customer::findByPhone($request->phone);
|
||||
if ($customer) {
|
||||
$customer_email = $customer->getMainEmail();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$customer) {
|
||||
// Create customer with passed name, email and phone
|
||||
if (Email::sanitizeEmail($request->to_email)) {
|
||||
|
||||
@@ -67,12 +67,26 @@ class CustomersController extends Controller
|
||||
//'emails' => 'array|required_without:first_name',
|
||||
//'emails.1' => 'nullable|email|required_without:first_name',
|
||||
'emails.*' => 'nullable|email|distinct|required_without:first_name',
|
||||
'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif',
|
||||
]);
|
||||
$validator->setAttributeNames([
|
||||
//'emails.1' => __('Email'),
|
||||
'photo_url' => __('Photo'),
|
||||
'emails.*' => __('Email'),
|
||||
]);
|
||||
|
||||
// Photo
|
||||
$validator->after(function ($validator) use ($customer, $request) {
|
||||
if ($request->hasFile('photo_url')) {
|
||||
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath(), $request->file('photo_url')->getMimeType());
|
||||
|
||||
if ($path_url) {
|
||||
$customer->photo_url = $path_url;
|
||||
} else {
|
||||
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('customers.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
@@ -128,11 +142,17 @@ class CustomersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$customer->fill($request->all());
|
||||
// Websites
|
||||
if (!empty($request->websites)) {
|
||||
$customer->setWebsites($request->websites);
|
||||
$request_data = $request->all();
|
||||
|
||||
if (isset($request_data['photo_url'])) {
|
||||
unset($request_data['photo_url']);
|
||||
}
|
||||
|
||||
$customer->setData($request_data);
|
||||
// Websites
|
||||
// if (!empty($request->websites)) {
|
||||
// $customer->setWebsites($request->websites);
|
||||
// }
|
||||
$customer->save();
|
||||
|
||||
$customer->syncEmails($request->emails);
|
||||
@@ -158,8 +178,12 @@ class CustomersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('customer.updated', $customer);
|
||||
|
||||
$flash_message = __('Customer saved successfully.').' '.$flash_message;
|
||||
\Session::flash('flash_success_unescaped', $flash_message);
|
||||
|
||||
\Session::flash('customer.updated', 1);
|
||||
|
||||
return redirect()->route('customers.update', ['id' => $id]);
|
||||
}
|
||||
@@ -206,6 +230,7 @@ class CustomersController extends Controller
|
||||
|
||||
$conversations = $customer->conversations()
|
||||
->where('customer_id', $customer->id)
|
||||
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
|
||||
@@ -358,6 +383,18 @@ class CustomersController extends Controller
|
||||
}
|
||||
break;
|
||||
|
||||
// Conversations navigation
|
||||
case 'customers_pagination':
|
||||
|
||||
$customers = app('App\Http\Controllers\ConversationsController')->searchCustomers($request, $user);
|
||||
|
||||
$response['status'] = 'success';
|
||||
|
||||
$response['html'] = view('customers/partials/customers_table', [
|
||||
'customers' => $customers,
|
||||
])->render();
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
|
||||
403
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
403
app/Http/Controllers/MailboxesController.php
Normal file → Executable file
@@ -29,9 +29,17 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function mailboxes()
|
||||
{
|
||||
//$this->authorize('create', 'App\Mailbox');
|
||||
//$mailboxes = Mailbox::all();
|
||||
$mailboxes = auth()->user()->mailboxesCanView();
|
||||
$user = auth()->user();
|
||||
|
||||
$mailboxes = $user->mailboxesCanView();
|
||||
|
||||
if (!\Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
|
||||
foreach ($mailboxes as $i => $mailbox) {
|
||||
if (!$user->canManageMailbox($mailbox->id)) {
|
||||
$mailboxes->forget($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('mailboxes/mailboxes', ['mailboxes' => $mailboxes]);
|
||||
}
|
||||
@@ -43,7 +51,7 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$this->authorize('create', 'App\Mailbox');
|
||||
|
||||
$users = User::where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
|
||||
return view('mailboxes/create', ['users' => $users]);
|
||||
}
|
||||
@@ -55,6 +63,8 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$this->authorize('create', 'App\Mailbox');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
@@ -64,7 +74,12 @@ class MailboxesController extends Controller
|
||||
|
||||
// //event(new Registered($user = $this->create($request->all())));
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (Mailbox::userEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('mailboxes.create')
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
@@ -88,9 +103,24 @@ class MailboxesController extends Controller
|
||||
public function update($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
//$this->authorize('update', $mailbox);
|
||||
if (!auth()->user()->can('update', $mailbox)) {
|
||||
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
|
||||
$user = auth()->user();
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
$accessible_route = '';
|
||||
|
||||
$mailbox_settings = $user->mailboxSettings($mailbox->id);
|
||||
$access_permissions = json_decode($mailbox_settings->access ?? '');
|
||||
|
||||
if ($access_permissions && is_array($access_permissions)) {
|
||||
foreach ($access_permissions as $perm) {
|
||||
$accessible_route = Mailbox::getAccessPermissionRoute($perm);
|
||||
if ($accessible_route) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$accessible_route) {
|
||||
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
|
||||
}
|
||||
if ($accessible_route) {
|
||||
return redirect()->route($accessible_route, ['id' => $mailbox->id]);
|
||||
} else {
|
||||
@@ -98,9 +128,17 @@ class MailboxesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
if (!$mailbox_user && $user->isAdmin()) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$user->mailboxes()->attach($id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
}
|
||||
|
||||
//$mailboxes = Mailbox::all()->except($id);
|
||||
|
||||
return view('mailboxes/update', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
return view('mailboxes/update', ['mailbox' => $mailbox, 'mailbox_user' => $mailbox_user, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,30 +149,63 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function updateSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:40',
|
||||
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
|
||||
'aliases' => 'nullable|string|max:255',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
'template' => 'required|integer',
|
||||
'ticket_assignee' => 'required|integer',
|
||||
'signature' => 'nullable|string',
|
||||
]);
|
||||
|
||||
//event(new Registered($user = $this->create($request->all())));
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||
\Helper::denyAccess();
|
||||
}
|
||||
|
||||
if ($user->can('updateSettings', $mailbox)) {
|
||||
|
||||
// if not admin, the text only fields don't pass so spike them into the request.
|
||||
if (!auth()->user()->isAdmin()) {
|
||||
$request->merge([
|
||||
'name' => $mailbox->name,
|
||||
'email' => $mailbox->email
|
||||
]);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:40',
|
||||
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
|
||||
'aliases' => 'nullable|string|max:255',
|
||||
'from_name' => 'required|integer',
|
||||
'from_name_custom' => 'nullable|string|max:128',
|
||||
'ticket_status' => 'required|integer',
|
||||
'template' => 'required|integer',
|
||||
'ticket_assignee' => 'required|integer',
|
||||
]);
|
||||
|
||||
//event(new Registered($user = $this->create($request->all())));
|
||||
if (Mailbox::userEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('mailboxes.update', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->can('updateEmailSignature', $mailbox)) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'signature' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('mailboxes.email_signature', ['id' => $id])
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
\Eventy::action('mailbox.settings_before_save', $mailbox, $request);
|
||||
|
||||
$mailbox->fill($request->all());
|
||||
|
||||
$mailbox->save();
|
||||
@@ -151,12 +222,25 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updatePermissions', $mailbox);
|
||||
|
||||
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
return view('mailboxes/permissions', ['mailbox' => $mailbox, 'users' => $users, 'mailbox_users' => $mailbox->users]);
|
||||
$managers = User::nonDeleted()
|
||||
->select(['users.*', 'mailbox_user.hide', 'mailbox_user.access'])
|
||||
->leftJoin('mailbox_user', function ($join) use ($mailbox) {
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
$join->where('mailbox_user.mailbox_id', $mailbox->id);
|
||||
})->get();
|
||||
$managers = User::sortUsers($managers);
|
||||
|
||||
return view('mailboxes/permissions', [
|
||||
'mailbox' => $mailbox,
|
||||
'users' => $users,
|
||||
'managers' => $managers,
|
||||
'mailbox_users' => $mailbox->users,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,11 +252,47 @@ class MailboxesController extends Controller
|
||||
public function permissionsSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updatePermissions', $mailbox);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$mailbox->users()->sync($request->users);
|
||||
$mailbox->syncPersonalFolders($request->users);
|
||||
|
||||
// Save admins settings.
|
||||
$admins = User::nonDeleted()->where('role', User::ROLE_ADMIN)->get();
|
||||
foreach ($admins as $admin) {
|
||||
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
if (!$mailbox_user) {
|
||||
// Admin may not be connected to the mailbox yet
|
||||
$admin->mailboxes()->attach($id);
|
||||
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
}
|
||||
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
|
||||
$mailbox_user->settings->save();
|
||||
}
|
||||
|
||||
// Sets the mailbox_user.access array
|
||||
$mailbox_users = $mailbox->users;
|
||||
foreach ($mailbox_users as $mailbox_user) {
|
||||
$access = [];
|
||||
$mailbox_with_settings = $mailbox_user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||
|
||||
foreach (\App\Mailbox::$access_permissions as $perm) {
|
||||
if (!empty($request->managers[$mailbox_user->id]['access'][$perm])) {
|
||||
$access[] = $request->managers[$mailbox_user->id]['access'][$perm];
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->id == $mailbox_user->id && !$user->isAdmin()) {
|
||||
// User with Permission priv's can't edit their own additional priv's.
|
||||
} else {
|
||||
$mailbox_with_settings->settings->access = json_encode($access);
|
||||
}
|
||||
$mailbox_with_settings->settings->hide = (isset($request->managers[$mailbox_user->id]['hide']) ? (int)$request->managers[$mailbox_user->id]['hide'] : false);
|
||||
$mailbox_with_settings->settings->save();
|
||||
}
|
||||
|
||||
\Session::flash('flash_success_floating', __('Mailbox permissions saved!'));
|
||||
|
||||
return redirect()->route('mailboxes.permissions', ['id' => $id]);
|
||||
@@ -184,7 +304,7 @@ class MailboxesController extends Controller
|
||||
public function connectionOutgoing($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
return view('mailboxes/connection', ['mailbox' => $mailbox, 'sendmail_path' => ini_get('sendmail_path'), 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||
}
|
||||
@@ -195,7 +315,7 @@ class MailboxesController extends Controller
|
||||
public function connectionOutgoingSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
if ($request->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
@@ -228,7 +348,7 @@ class MailboxesController extends Controller
|
||||
|
||||
// Sometimes background job continues to use old connection settings.
|
||||
\Helper::queueWorkRestart();
|
||||
|
||||
|
||||
\Session::flash('flash_success_floating', __('Connection settings saved!'));
|
||||
|
||||
return redirect()->route('mailboxes.connection', ['id' => $id]);
|
||||
@@ -237,16 +357,16 @@ class MailboxesController extends Controller
|
||||
/**
|
||||
* Mailbox incoming settings.
|
||||
*/
|
||||
public function connectionIncoming($id)
|
||||
public function connectionIncoming($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
$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, [
|
||||
@@ -265,7 +385,7 @@ class MailboxesController extends Controller
|
||||
public function connectionIncomingSave($id, Request $request)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
// $validator = Validator::make($request->all(), [
|
||||
// 'in_server' => 'nullable|string|max:255',
|
||||
@@ -286,19 +406,21 @@ 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.
|
||||
// Save all custom folders except INBOX.
|
||||
$in_imap_folders = [];
|
||||
foreach ($request->in_imap_folders as $imap_folder) {
|
||||
if (mb_strtolower($imap_folder) != 'inbox') {
|
||||
if (is_array($request->in_imap_folders)) {
|
||||
foreach ($request->in_imap_folders as $imap_folder) {
|
||||
$in_imap_folders[] = $imap_folder;
|
||||
}
|
||||
}
|
||||
@@ -316,10 +438,14 @@ class MailboxesController extends Controller
|
||||
*/
|
||||
public function view($id, $folder_id = null)
|
||||
{
|
||||
//auth()->guard()->attempt(['email' => '', 'password' => '']);
|
||||
$user = auth()->user();
|
||||
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('view', $mailbox);
|
||||
if ($user->isAdmin()) {
|
||||
$mailbox = Mailbox::findOrFailWithSettings($id, $user->id);
|
||||
} else {
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
}
|
||||
$this->authorize('viewCached', $mailbox);
|
||||
|
||||
$folders = $mailbox->getAssesibleFolders();
|
||||
|
||||
@@ -338,8 +464,6 @@ class MailboxesController extends Controller
|
||||
|
||||
$this->authorize('view', $folder);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
|
||||
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||
|
||||
@@ -355,7 +479,7 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$flashes = [];
|
||||
|
||||
if ($mailbox) {
|
||||
if ($mailbox && \Auth::user()->can('admin', $mailbox)) {
|
||||
if (Route::currentRouteName() != 'mailboxes.connection' && !$mailbox->isOutActive()) {
|
||||
$flashes[] = [
|
||||
'type' => 'warning',
|
||||
@@ -381,7 +505,7 @@ class MailboxesController extends Controller
|
||||
public function autoReply($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('update', $mailbox);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
if (!$mailbox->auto_reply_subject) {
|
||||
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||
@@ -399,7 +523,8 @@ class MailboxesController extends Controller
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $mailbox);
|
||||
// $this->authorize('update', $mailbox);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
$request->merge([
|
||||
'auto_reply_enabled' => ($request->filled('auto_reply_enabled') ?? false),
|
||||
@@ -433,6 +558,24 @@ class MailboxesController extends Controller
|
||||
return redirect()->route('mailboxes.auto_reply', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto reply settings.
|
||||
*/
|
||||
public function emailSignature($id)
|
||||
{
|
||||
$mailbox = Mailbox::findOrFail($id);
|
||||
$this->authorize('updateAutoReply', $mailbox);
|
||||
|
||||
if (!$mailbox->auto_reply_subject) {
|
||||
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||
}
|
||||
|
||||
return view('mailboxes/email_signature', [
|
||||
'mailbox' => $mailbox,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Users ajax controller.
|
||||
*/
|
||||
@@ -453,14 +596,14 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} elseif (empty($request->to)) {
|
||||
$response['msg'] = __('Please specify recipient of the test email');
|
||||
}
|
||||
|
||||
// Check if outgoing port is open.
|
||||
if (!$response['msg']) {
|
||||
if (!$response['msg'] && $mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
$test_result = \Helper::checkPort($mailbox->out_server, $mailbox->out_port);
|
||||
if (!$test_result) {
|
||||
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->out_server.'</strong>', 'port' => '<strong>'.$mailbox->out_port.'</strong>']);
|
||||
@@ -477,7 +620,7 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
|
||||
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,19 +640,23 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
$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 {
|
||||
@@ -519,11 +666,11 @@ class MailboxesController extends Controller
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend connecting to the server');
|
||||
$response['msg'] = __('Error occurred connecting to the server');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
if (!$response['msg'] && !$tested) {
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
@@ -534,7 +681,7 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
@@ -586,7 +733,7 @@ class MailboxesController extends Controller
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->can('update', $mailbox)) {
|
||||
} elseif (!$user->can('admin', $mailbox)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
} elseif (!Hash::check($request->password, $user->password)) {
|
||||
$response['msg'] = __('Please double check your password, and try again');
|
||||
@@ -610,6 +757,31 @@ class MailboxesController extends Controller
|
||||
}
|
||||
break;
|
||||
|
||||
// Mute notifications
|
||||
case 'mute':
|
||||
$mailbox = Mailbox::find($request->mailbox_id);
|
||||
|
||||
if (!$mailbox) {
|
||||
$response['msg'] = __('Mailbox not found');
|
||||
} elseif (!$user->isAdmin()) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
if (!$mailbox_user) {
|
||||
// User may not be connected to the mailbox yet
|
||||
$user->mailboxes()->attach($mailbox->id);
|
||||
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||
}
|
||||
$mailbox_user->settings->mute = (bool)$request->mute;
|
||||
$mailbox_user->settings->save();
|
||||
|
||||
$response['status'] = 'success';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['msg'] = 'Unknown action';
|
||||
break;
|
||||
@@ -621,4 +793,107 @@ class MailboxesController extends Controller
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
|
||||
public function oauth(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
|
||||
$state_data = [];
|
||||
if (!empty($request->state)) {
|
||||
$state_data = json_decode($request->state, true);
|
||||
if (!empty($state_data['mailbox_id'])) {
|
||||
$mailbox_id = $state_data['mailbox_id'];
|
||||
}
|
||||
if (!empty($state_data['provider'])) {
|
||||
$provider = $state_data['provider'];
|
||||
}
|
||||
}
|
||||
|
||||
// MS Exchange.
|
||||
if (!empty($request->error) && $request->error == 'invalid_request' && !empty($request->error_description)) {
|
||||
return htmlspecialchars($request->error_description);
|
||||
}
|
||||
|
||||
if (empty($provider)) {
|
||||
return 'Invalid oAuth Provider';
|
||||
}
|
||||
|
||||
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
if (empty($mailbox)) {
|
||||
return __('Mailbox not found').': '.$mailbox_id;
|
||||
}
|
||||
if (empty($mailbox->in_username)) {
|
||||
return 'Enter oAuth Client ID as Username and save mailbox settings';
|
||||
}
|
||||
if (empty($mailbox->in_password)) {
|
||||
return 'Enter oAuth Client Secret as Password and save mailbox settings';
|
||||
}
|
||||
|
||||
$session_data = [];
|
||||
if (\Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id)) {
|
||||
$session_data = \Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||
}
|
||||
|
||||
if (empty($request->code)) {
|
||||
$state = [
|
||||
'provider' => $provider,
|
||||
'mailbox_id' => $mailbox_id,
|
||||
'state' => crc32($mailbox->in_username.$mailbox->in_password),
|
||||
];
|
||||
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'state' => json_encode($state),
|
||||
'client_id' => $mailbox->in_username,
|
||||
]);
|
||||
if ($url) {
|
||||
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
|
||||
// [
|
||||
// 'provider' => $request->provider,
|
||||
// 'mailbox_id' => $request->mailbox_id,
|
||||
// 'state' => $provider->getState(),
|
||||
// ]);
|
||||
return redirect()->away($url);
|
||||
} else {
|
||||
return 'Could not generate authorization URL: check Client ID (Username) and Client Secret (Password)';
|
||||
}
|
||||
|
||||
// Check given state against previously stored one to mitigate CSRF attack
|
||||
} elseif (empty($request->state) || ($state_data['state'] ?? '') !== ($session_data['state'] ?? '')) {
|
||||
|
||||
\Session::forget('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||
return 'Invalid oAuth state';
|
||||
|
||||
} else {
|
||||
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
return __('Error occurred').': '.htmlspecialchars($token_data['error']);
|
||||
}
|
||||
|
||||
return redirect()->route('mailboxes.connection.incoming', ['id' => $mailbox_id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function oauthDisconnect(Request $request)
|
||||
{
|
||||
$mailbox_id = $request->id ?? '';
|
||||
$provider = $request->provider ?? '';
|
||||
|
||||
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||
$this->authorize('admin', $mailbox);
|
||||
|
||||
// oAuth Disconnect.
|
||||
$mailbox->removeMetaParam('oauth', true);
|
||||
return \MailHelper::oauthDisconnect($provider, route('mailboxes.connection.incoming', ['id' => $mailbox_id]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ class ModulesController extends Controller
|
||||
{
|
||||
$installed_modules = [];
|
||||
$modules_directory = [];
|
||||
$third_party_modules = [];
|
||||
$all_modules = [];
|
||||
$flashes = [];
|
||||
$updates_available = false;
|
||||
|
||||
@@ -53,8 +55,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(),
|
||||
@@ -66,7 +67,7 @@ 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')),
|
||||
@@ -74,6 +75,8 @@ class ModulesController extends Controller
|
||||
// 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
|
||||
@@ -85,10 +88,14 @@ class ModulesController extends Controller
|
||||
// Prepare directory modules
|
||||
if (is_array($modules_directory)) {
|
||||
foreach ($modules_directory as $i_dir => $dir_module) {
|
||||
|
||||
$modules_directory[$i_dir] = \App\Module::formatModuleData($dir_module);
|
||||
|
||||
// Remove modules without aliases
|
||||
if (empty($dir_module['alias'])) {
|
||||
unset($modules_directory[$i_dir]);
|
||||
}
|
||||
$all_modules[$dir_module['alias']] = $dir_module['name'];
|
||||
foreach ($installed_modules as $i_installed => $module) {
|
||||
if ($dir_module['alias'] == $module['alias']) {
|
||||
// Set image from director
|
||||
@@ -116,6 +123,12 @@ 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 Derectory.
|
||||
if (\App\Module::isThirdParty($dir_module)) {
|
||||
$third_party_modules[] = $modules_directory[$i_dir];
|
||||
unset($modules_directory[$i_dir]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$modules_directory = [];
|
||||
@@ -124,8 +137,10 @@ class ModulesController extends Controller
|
||||
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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -279,6 +294,22 @@ class ModulesController extends Controller
|
||||
$response['msg'] = __('Your domain is deactivated');
|
||||
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'] = $this->getErrorMessage($result['error'], $result);
|
||||
} else {
|
||||
// Some unknown error. Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +339,7 @@ class ModulesController extends Controller
|
||||
$type = 'success';
|
||||
$msg = __('":name" module successfully activated!', ['name' => $name]);
|
||||
} else {
|
||||
// Deactivate module
|
||||
// Deactivate the module.
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache');
|
||||
}
|
||||
@@ -324,6 +355,11 @@ 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', ['--force' => true]);
|
||||
}
|
||||
|
||||
// \Session::flash does not work after BufferedOutput
|
||||
$flash = [
|
||||
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||
@@ -380,7 +416,7 @@ class ModulesController extends Controller
|
||||
$params = [
|
||||
'license' => $license,
|
||||
'module_alias' => $alias,
|
||||
'url' => \App\Module::getAppUrl(),
|
||||
'url' => (!empty($request->any_url) ? '*' : \App\Module::getAppUrl()),
|
||||
];
|
||||
$result = WpApi::deactivateLicense($params);
|
||||
|
||||
@@ -390,12 +426,15 @@ class ModulesController extends Controller
|
||||
$response['msg'] = $result['message'];
|
||||
} else {
|
||||
if (!empty($result['status']) && $result['status'] == 'success') {
|
||||
// Remove remembered license key and deactivate license in DB
|
||||
\App\Module::deactivateLicense($alias, '');
|
||||
$db_module = \App\Module::getByAlias($alias);
|
||||
if ($db_module && trim($db_module->license ?? '') == trim($license ?? '')) {
|
||||
// Remove remembered license key and deactivate license in DB
|
||||
\App\Module::deactivateLicense($alias, '');
|
||||
|
||||
// Deactivate module
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache', []);
|
||||
// Deactivate module
|
||||
\App\Module::setActive($alias, false);
|
||||
\Artisan::call('freescout:clear-cache', []);
|
||||
}
|
||||
|
||||
// Flash does not work here.
|
||||
$flash = [
|
||||
@@ -574,7 +613,7 @@ class ModulesController extends Controller
|
||||
$msg = __('License key has been revoked');
|
||||
break;
|
||||
case 'no_activations_left':
|
||||
$msg = __('No activations left for this license key');
|
||||
$msg = __('No activations left for this license key').' ('.__('Use Deactivate License button to transfer license key from another domain').')';
|
||||
break;
|
||||
case 'expired':
|
||||
$msg = __('License key has expired');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Attachment;
|
||||
use App\Conversation;
|
||||
use App\Option;
|
||||
use App\Thread;
|
||||
@@ -33,6 +34,10 @@ class PublicController extends Controller
|
||||
}
|
||||
$user = User::where('invite_hash', $hash)->first();
|
||||
|
||||
if ($user && $user->locale) {
|
||||
\Helper::setLocale($user->locale);
|
||||
}
|
||||
|
||||
return view('public/user_setup', ['user' => $user]);
|
||||
}
|
||||
|
||||
@@ -96,7 +101,8 @@ class PublicController extends Controller
|
||||
|
||||
$user->invite_state = User::INVITE_STATE_ACTIVATED;
|
||||
$user->invite_hash = '';
|
||||
|
||||
|
||||
$user = \Eventy::filter('user.setup_save', $user, $request);
|
||||
$user->save();
|
||||
|
||||
// Login user
|
||||
@@ -123,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);
|
||||
@@ -134,4 +140,75 @@ class PublicController extends Controller
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an attachment.
|
||||
*/
|
||||
public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $request)
|
||||
{
|
||||
$id = $request->query('id', '');
|
||||
$token = $request->query('token', '');
|
||||
$attachment = null;
|
||||
|
||||
// Old attachments can not be requested by id.
|
||||
if (!$token && $id) {
|
||||
return \Helper::denyAccess();
|
||||
}
|
||||
|
||||
// Get attachment by id.
|
||||
if ($id) {
|
||||
$attachment = Attachment::findOrFail($id);
|
||||
}
|
||||
|
||||
if (!$attachment) {
|
||||
$attachment = Attachment::where('file_dir', $dir_1.DIRECTORY_SEPARATOR.$dir_2.DIRECTORY_SEPARATOR.$dir_3.DIRECTORY_SEPARATOR)
|
||||
->where('file_name', $file_name)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
// Only allow download if the attachment is public or if the token matches the hash of the contents
|
||||
if ($token != $attachment->getToken() && (bool)$attachment->public !== true) {
|
||||
return \Helper::denyAccess();
|
||||
}
|
||||
|
||||
$view_attachment = false;
|
||||
$file_ext = strtolower(pathinfo($attachment->file_name, PATHINFO_EXTENSION));
|
||||
|
||||
// Some file type should be viewed in the browser instead of downloading.
|
||||
if (in_array($file_ext, config('app.viewable_attachments'))) {
|
||||
$view_attachment = true;
|
||||
}
|
||||
|
||||
if (config('app.download_attachments_via') == 'apache') {
|
||||
// Send using Apache mod_xsendfile.
|
||||
$response = response(null)
|
||||
->header('Content-Type' , $attachment->mime_type)
|
||||
->header('X-Sendfile', $attachment->getLocalFilePath());
|
||||
|
||||
if (!$view_attachment) {
|
||||
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||
}
|
||||
} elseif (config('app.download_attachments_via') == 'nginx') {
|
||||
// Send using Nginx.
|
||||
$response = response(null)
|
||||
->header('Content-Type' , $attachment->mime_type)
|
||||
->header('X-Accel-Redirect', $attachment->getLocalFilePath(false));
|
||||
|
||||
if (!$view_attachment) {
|
||||
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||
}
|
||||
} else {
|
||||
$response = $attachment->download($view_attachment);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Needed for the mobile app.
|
||||
*/
|
||||
public function mobilePing()
|
||||
{
|
||||
echo file_get_contents(public_path('installer/css/fontawesome.css'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\ActivityLog;
|
||||
use App\Misc\Helper;
|
||||
use App\SendLog;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
@@ -27,7 +28,15 @@ class SecureController extends Controller
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
$mailboxes = auth()->user()->mailboxesCanView();
|
||||
$user = auth()->user();
|
||||
if (!$user->isAdmin()) {
|
||||
$mailboxes = $user->mailboxesCanView();
|
||||
} else {
|
||||
$mailboxes = $user->mailboxesCanViewWithSettings();
|
||||
}
|
||||
|
||||
// Sort by name.
|
||||
$mailboxes = \Eventy::filter('dashboard.mailboxes', $mailboxes->sortBy('name'));
|
||||
|
||||
return view('secure/dashboard', ['mailboxes' => $mailboxes]);
|
||||
}
|
||||
@@ -119,6 +128,9 @@ class SecureController extends Controller
|
||||
$status = $record->getStatusName();
|
||||
if ($record->status_message) {
|
||||
$status .= '. '.$record->status_message;
|
||||
if ($record->status == SendLog::STATUS_SEND_ERROR) {
|
||||
$status .= '. Message-ID: '.$record->message_id;
|
||||
}
|
||||
}
|
||||
|
||||
$logs[] = [
|
||||
@@ -158,7 +170,7 @@ class SecureController extends Controller
|
||||
|
||||
$name = '';
|
||||
if (!empty($request->name)) {
|
||||
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
|
||||
//$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
|
||||
$name = $request->name;
|
||||
} elseif (count($names = ActivityLog::select('log_name')->distinct()->get()->pluck('log_name'))) {
|
||||
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||
@@ -177,4 +189,41 @@ class SecureController extends Controller
|
||||
|
||||
return redirect()->route('logs', ['name' => $name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files and images.
|
||||
*/
|
||||
public function upload(Request $request, $allowed_exts = [])
|
||||
{
|
||||
// 'jpg','gif','png'
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'msg' => '', // this is error message
|
||||
];
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
$response['msg'] = __('Please login to upload file');
|
||||
}
|
||||
|
||||
if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) {
|
||||
$response['msg'] = __('Error occured uploading file');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
|
||||
$upload = Helper::uploadFile($request->file, $allowed_exts);
|
||||
$filename = basename($upload);
|
||||
|
||||
if ($upload) {
|
||||
$response['status'] = 'success';
|
||||
$response['url'] = Helper::uploadedFileUrl($filename);
|
||||
} else {
|
||||
$response['msg'] = __('Error occured uploading file');
|
||||
}
|
||||
}
|
||||
|
||||
return \Response::json($response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Option;
|
||||
use App\Subscription;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Validator;
|
||||
@@ -100,7 +101,8 @@ class SettingsController extends Controller
|
||||
'env' => 'APP_FETCH_SCHEDULE',
|
||||
],
|
||||
'mail_password' => [
|
||||
'safe_password' => true
|
||||
'safe_password' => true,
|
||||
'encrypt' => true,
|
||||
]
|
||||
],
|
||||
];
|
||||
@@ -108,19 +110,39 @@ class SettingsController extends Controller
|
||||
case 'general':
|
||||
$params = [
|
||||
'settings' => [
|
||||
'email_conv_history' => [
|
||||
'env' => 'APP_EMAIL_CONV_HISTORY',
|
||||
],
|
||||
'email_user_history' => [
|
||||
'env' => 'APP_EMAIL_USER_HISTORY',
|
||||
],
|
||||
'locale' => [
|
||||
'env' => 'APP_LOCALE',
|
||||
],
|
||||
'timezone' => [
|
||||
'env' => 'APP_TIMEZONE',
|
||||
],
|
||||
'user_permissions' => [
|
||||
'env' => 'APP_USER_PERMISSIONS',
|
||||
'env_encode' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
break;
|
||||
case 'alerts':
|
||||
$subscriptions_defaults = Subscription::getDefaultSubscriptions();
|
||||
$subscriptions = array();
|
||||
foreach ($subscriptions_defaults as $medium => $subscriptions_for_medium) {
|
||||
foreach ($subscriptions_defaults[$medium] as $subscription) {
|
||||
$subscriptions[] = (object) array("medium" => $medium, "event" => $subscription);
|
||||
}
|
||||
}
|
||||
$params = [
|
||||
'template_vars' => [
|
||||
'logs' => \App\ActivityLog::getAvailableLogs(),
|
||||
'person' => null,
|
||||
'subscriptions' => $subscriptions,
|
||||
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
|
||||
],
|
||||
'settings' => [
|
||||
'alert_logs' => [
|
||||
@@ -165,9 +187,11 @@ class SettingsController extends Controller
|
||||
$settings = [
|
||||
'company_name' => Option::get('company_name', \Config::get('app.name')),
|
||||
'next_ticket' => (Option::get('next_ticket') >= Conversation::max('number') + 1) ? Option::get('next_ticket') : Conversation::max('number') + 1,
|
||||
'user_permissions' => Option::get('user_permissions', []),
|
||||
'user_permissions' => User::getGlobalUserPermissions(),
|
||||
'email_branding' => Option::get('email_branding'),
|
||||
'open_tracking' => Option::get('open_tracking'),
|
||||
'email_conv_history' => config('app.email_conv_history'),
|
||||
'email_user_history' => config('app.email_user_history'),
|
||||
'enrich_customer_data' => Option::get('enrich_customer_data'),
|
||||
'time_format' => Option::get('time_format', User::TIME_FORMAT_24),
|
||||
'locale' => \Helper::getRealAppLocale(),
|
||||
@@ -181,7 +205,7 @@ 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'),
|
||||
];
|
||||
@@ -194,6 +218,7 @@ class SettingsController extends Controller
|
||||
'alert_logs',
|
||||
'alert_logs_names',
|
||||
'alert_logs_period',
|
||||
'subscription_defaults',
|
||||
], [
|
||||
'alert_logs_names' => [],
|
||||
'alert_logs' => config('app.alert_logs'),
|
||||
@@ -267,7 +292,21 @@ class SettingsController extends Controller
|
||||
// Option has to be saved to .env file.
|
||||
if (!empty($settings_params[$option_name]) && !empty($settings_params[$option_name]['env'])) {
|
||||
$env_value = $request->settings[$option_name] ?? '';
|
||||
|
||||
if (is_array($env_value)) {
|
||||
$env_value = json_encode($env_value);
|
||||
}
|
||||
|
||||
if (!empty($settings_params[$option_name]['encrypt'])) {
|
||||
$env_value = encrypt($env_value);
|
||||
}
|
||||
|
||||
if (!empty($settings_params[$option_name]['env_encode'])) {
|
||||
$env_value = base64_encode($env_value);
|
||||
}
|
||||
|
||||
\Helper::setEnvFileVar($settings_params[$option_name]['env'], $env_value);
|
||||
|
||||
config($option_name, $env_value);
|
||||
$cc_required = true;
|
||||
continue;
|
||||
@@ -276,6 +315,11 @@ class SettingsController extends Controller
|
||||
// By some reason isset() does not work for empty elements.
|
||||
if (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,
|
||||
@@ -300,9 +344,14 @@ class SettingsController extends Controller
|
||||
\Helper::clearCache(['--doNotGenerateVars' => true]);
|
||||
}
|
||||
|
||||
\Session::flash('flash_success_floating', __('Settings updated'));
|
||||
// \Helper::clearCache prevents \Session::flash() from displaying.
|
||||
$request->session()->flash('flash_success_floating', __('Settings updated'));
|
||||
|
||||
return redirect()->route('settings', ['section' => $section]);
|
||||
$response = redirect()->route('settings', ['section' => $section]);
|
||||
|
||||
$response = \Eventy::filter('settings.after_save', $response, $request, $section, $settings);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,7 +385,7 @@ class SettingsController extends Controller
|
||||
}
|
||||
|
||||
if (!$test_result && !$response['msg']) {
|
||||
$response['msg'] = __('Error occurend sending email. Please check your mail server logs for more details.');
|
||||
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ class SystemController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('auth', ['except' => [
|
||||
'cron'
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,6 +45,13 @@ class SystemController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
];
|
||||
|
||||
// Permissions.
|
||||
$permissions = [];
|
||||
foreach (config('installer.permissions') as $perm_path => $perm_value) {
|
||||
@@ -56,6 +65,18 @@ class SystemController extends Controller
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
// Check if cache files are writable.
|
||||
$non_writable_cache_file = '';
|
||||
if (function_exists('shell_exec')) {
|
||||
$non_writable_cache_file = 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 ?? '');
|
||||
if (!strstr($non_writable_cache_file, base_path('storage/framework/cache/data/'))) {
|
||||
$non_writable_cache_file = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if public symlink exists, if not, try to create.
|
||||
$public_symlink_exists = true;
|
||||
$public_path = public_path('storage');
|
||||
@@ -79,7 +100,7 @@ class SystemController extends Controller
|
||||
|
||||
// Commands
|
||||
$commands_list = [
|
||||
'freescout:fetch-emails' => 'freescout:fetch-emails',
|
||||
'freescout:fetch-emails' => 'freescout:fetch-emails',
|
||||
\Helper::getWorkerIdentifier() => 'queue:work'
|
||||
];
|
||||
foreach ($commands_list as $command_identifier => $command_name) {
|
||||
@@ -168,19 +189,23 @@ class SystemController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
// Check new version
|
||||
// Check new version if enabled
|
||||
$new_version_available = false;
|
||||
$latest_version = \Cache::remember('latest_version', 15, function () {
|
||||
try {
|
||||
return \Updater::getVersionAvailable();
|
||||
} catch (\Exception $e) {
|
||||
SystemController::$latest_version_error = $e->getMessage();
|
||||
return '';
|
||||
}
|
||||
});
|
||||
if (!\Config::get('app.disable_updating')) {
|
||||
$latest_version = \Cache::remember('latest_version', 15, function () {
|
||||
try {
|
||||
return \Updater::getVersionAvailable();
|
||||
} catch (\Exception $e) {
|
||||
SystemController::$latest_version_error = $e->getMessage();
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
|
||||
$new_version_available = true;
|
||||
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
|
||||
$new_version_available = true;
|
||||
}
|
||||
} else {
|
||||
$latest_version = \Config::get('app.version');
|
||||
}
|
||||
|
||||
return view('system/status', [
|
||||
@@ -189,18 +214,25 @@ class SystemController extends Controller
|
||||
'failed_jobs' => $failed_jobs,
|
||||
'failed_queues' => $failed_queues,
|
||||
'php_extensions' => $php_extensions,
|
||||
'functions' => $functions,
|
||||
'permissions' => $permissions,
|
||||
'new_version_available' => $new_version_available,
|
||||
'latest_version' => $latest_version,
|
||||
'latest_version_error' => SystemController::$latest_version_error,
|
||||
'public_symlink_exists' => $public_symlink_exists,
|
||||
'env_is_writable' => $env_is_writable,
|
||||
'non_writable_cache_file' => $non_writable_cache_file,
|
||||
]);
|
||||
}
|
||||
|
||||
public function action(Request $request)
|
||||
{
|
||||
switch ($request->action) {
|
||||
case 'cancel_job':
|
||||
\App\Job::where('id', $request->job_id)->delete();
|
||||
\Session::flash('flash_success_floating', __('Done'));
|
||||
break;
|
||||
|
||||
case 'delete_failed_jobs':
|
||||
\App\FailedJob::where('queue', $request->failed_queue)->delete();
|
||||
\Session::flash('flash_success_floating', __('Failed jobs deleted'));
|
||||
@@ -259,6 +291,10 @@ class SystemController extends Controller
|
||||
case 'migrate_db':
|
||||
\Artisan::call('migrate', ['--force' => true], $outputLog);
|
||||
break;
|
||||
|
||||
case 'logout_users':
|
||||
\Artisan::call('freescout:logout-users', [], $outputLog);
|
||||
break;
|
||||
}
|
||||
|
||||
$output = $outputLog->fetch();
|
||||
@@ -287,7 +323,7 @@ class SystemController extends Controller
|
||||
case 'update':
|
||||
try {
|
||||
$status = \Updater::update();
|
||||
|
||||
|
||||
// Artisan::output()
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured. Please try again or try another :%a_start%update method:%a_end%', ['%a_start%' => '<a href="'.config('app.freescout_url').'/docs/update/" target="_blank">', '%a_end%' => '</a>']);
|
||||
@@ -303,14 +339,18 @@ class SystemController extends Controller
|
||||
break;
|
||||
|
||||
case 'check_updates':
|
||||
try {
|
||||
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured').': '.$e->getMessage();
|
||||
}
|
||||
if (!$response['msg'] && !$response['new_version_available']) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
if (!\Config::get('app.disable_updating')) {
|
||||
try {
|
||||
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||
$response['status'] = 'success';
|
||||
} catch (\Exception $e) {
|
||||
$response['msg'] = __('Error occured').': '.$e->getMessage();
|
||||
}
|
||||
if (!$response['msg'] && !$response['new_version_available']) {
|
||||
// Adding session flash is useless as cache is cleated
|
||||
$response['msg_success'] = __('You have the latest version installed');
|
||||
}
|
||||
} else {
|
||||
$response['msg_success'] = __('You have the latest version installed');
|
||||
}
|
||||
break;
|
||||
@@ -326,4 +366,19 @@ class SystemController extends Controller
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use \Barryvdh\TranslationManager\Models\Translation;
|
||||
use Barryvdh\TranslationManager\Controller as BaseController;
|
||||
|
||||
class TranslateController extends BaseController
|
||||
@@ -15,6 +16,13 @@ class TranslateController extends BaseController
|
||||
{
|
||||
$result = false;
|
||||
|
||||
// Count changed translations.
|
||||
$changed_data = Translation::select(['locale', 'group', \DB::raw('count(*) as changed')])
|
||||
->where('status', Translation::STATUS_CHANGED)
|
||||
->groupBy(['locale', 'group'])
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$this->manager->exportTranslations('*', false);
|
||||
|
||||
// Archive langs folder
|
||||
@@ -31,7 +39,7 @@ class TranslateController extends BaseController
|
||||
$attachments[] = $archive_path;
|
||||
|
||||
// Send archive to developers
|
||||
$result = \MailHelper::sendEmailToDevs('Translations', '', $attachments, auth()->user());
|
||||
$result = \MailHelper::sendEmailToDevs('Translations', json_encode($changed_data), $attachments, auth()->user());
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
|
||||
@@ -57,21 +57,30 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function createSave(Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
$this->authorize('create', 'App\User');
|
||||
$auth_user = auth()->user();
|
||||
|
||||
$rules = [
|
||||
'role' => 'integer',
|
||||
'first_name' => 'required|string|max:20',
|
||||
'last_name' => 'required|string|max:30',
|
||||
'email' => 'required|string|email|max:100|unique:users',
|
||||
'role' => ['required', Rule::in(array_keys(User::$roles))],
|
||||
//'role' => ['required', Rule::in(array_keys(User::$roles))],
|
||||
];
|
||||
if ($auth_user->isAdmin()) {
|
||||
$rules['role'] = ['required', Rule::in(array_keys(User::$roles))];
|
||||
}
|
||||
if (empty($request->send_invite)) {
|
||||
$rules['password'] = 'required|string|max:255';
|
||||
}
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
if (User::mailboxEmailExists($request->email)) {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
|
||||
}
|
||||
|
||||
if ($invalid || $validator->fails()) {
|
||||
return redirect()->route('users.create')
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
@@ -79,6 +88,9 @@ class UsersController extends Controller
|
||||
|
||||
$user = new User();
|
||||
$user->fill($request->all());
|
||||
if (!$auth_user->can('changeRole', $user)) {
|
||||
$user->role = User::ROLE_USER;
|
||||
}
|
||||
if (empty($request->send_invite)) {
|
||||
// Set password from request
|
||||
$user->password = Hash::make($request->password);
|
||||
@@ -86,6 +98,7 @@ class UsersController extends Controller
|
||||
// Set some random password before sending invite
|
||||
$user->password = Hash::make($user->generateRandomPassword());
|
||||
}
|
||||
$user = \Eventy::filter('user.create_save', $user, $request);
|
||||
$user->save();
|
||||
|
||||
$user->mailboxes()->sync($request->mailboxes);
|
||||
@@ -141,6 +154,8 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function profileSave($id, Request $request)
|
||||
{
|
||||
$invalid = false;
|
||||
|
||||
$user = User::findOrFail($id);
|
||||
$this->authorize('update', $user);
|
||||
|
||||
@@ -149,7 +164,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',
|
||||
@@ -169,6 +184,7 @@ class UsersController extends Controller
|
||||
if ($path_url) {
|
||||
$user->photo_url = $path_url;
|
||||
} else {
|
||||
$invalid = true;
|
||||
$validator->errors()->add('photo_url', __('Error occured processing the image. Make sure that PHP GD extension is enabled.'));
|
||||
}
|
||||
}
|
||||
@@ -177,12 +193,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();
|
||||
@@ -201,12 +223,21 @@ class UsersController extends Controller
|
||||
if (!auth()->user()->can('changeRole', $user)) {
|
||||
unset($request_data['role']);
|
||||
}
|
||||
$user->fill($request_data);
|
||||
if ($user->status != User::STATUS_DELETED) {
|
||||
if (!empty($request_data['disabled'])) {
|
||||
$request_data['status'] = User::STATUS_DISABLED;
|
||||
} else {
|
||||
$request_data['status'] = User::STATUS_ACTIVE;
|
||||
}
|
||||
}
|
||||
$user->setData($request_data);
|
||||
|
||||
if (empty($request->input('enable_kb_shortcuts'))) {
|
||||
$user->enable_kb_shortcuts = false;
|
||||
}
|
||||
|
||||
$user = \Eventy::filter('user.save_profile', $user, $request);
|
||||
|
||||
$user->save();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Profile saved successfully'));
|
||||
@@ -256,6 +287,21 @@ class UsersController extends Controller
|
||||
$user->mailboxes()->sync($request->mailboxes);
|
||||
$user->syncPersonalFolders($request->mailboxes);
|
||||
|
||||
// Save permissions.
|
||||
$user_permissions = $request->user_permissions ?? [];
|
||||
$permissions = [];
|
||||
|
||||
foreach (User::getUserPermissionsList() as $permission_id) {
|
||||
$new_has_permission = in_array($permission_id, $user_permissions);
|
||||
|
||||
if ($user->hasPermission($permission_id, false) != $new_has_permission) {
|
||||
$permissions[$permission_id] = (int)(bool)$new_has_permission;
|
||||
$save_user = true;
|
||||
}
|
||||
}
|
||||
$user->permissions = $permissions;
|
||||
$user->save();
|
||||
|
||||
\Session::flash('flash_success_floating', __('Permissions saved successfully'));
|
||||
|
||||
return redirect()->route('users.permissions', ['id' => $id]);
|
||||
@@ -512,7 +558,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;
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
// todo: These two may need to be moved into 'web' not to be called in 'api'.
|
||||
\App\Http\Middleware\ResponseHeaders::class,
|
||||
\App\Http\Middleware\TerminateHandler::class,
|
||||
];
|
||||
@@ -45,10 +44,10 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\CustomHandle::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
],
|
||||
// 'api' => [
|
||||
// 'throttle:60,1',
|
||||
// 'bindings',
|
||||
// ],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,6 @@ class CustomHandle
|
||||
// Hook.
|
||||
\Eventy::action('middleware.web.custom_handle', $request);
|
||||
|
||||
return $next($request);
|
||||
return \Eventy::filter('middleware.web.custom_handle.response', $next($request), $request, $next);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ class HttpsRedirect {
|
||||
return redirect()->secure($request->getRequestUri());
|
||||
}
|
||||
}
|
||||
|
||||
// Correct protocol in $_SERVER
|
||||
if (parse_url(config('app.url'), PHP_URL_SCHEME) == 'https'
|
||||
&& !$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);
|
||||
|
||||
@@ -20,7 +20,7 @@ class LogoutIfDeleted
|
||||
public function handle($request, Closure $next, $guard = null)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if ($user && $user->status == \App\User::STATUS_DELETED) {
|
||||
if ($user && ($user->isDeleted() || $user->isDisabled())) {
|
||||
Auth::logout();
|
||||
return Redirect::route('login');
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Job extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
/**
|
||||
* Automatically converted into Carbon dates.
|
||||
*/
|
||||
|
||||
@@ -44,7 +44,7 @@ class SendAutoReply implements ShouldQueue
|
||||
public function handle()
|
||||
{
|
||||
// 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.'>';
|
||||
@@ -55,6 +55,11 @@ class SendAutoReply implements ShouldQueue
|
||||
$headers['Message-ID'] = $message_id;
|
||||
|
||||
$customer_email = $this->conversation->customer_email;
|
||||
|
||||
if (!$customer_email) {
|
||||
// When message is received via Chat, customer has no email adddress.
|
||||
return;
|
||||
}
|
||||
$recipients = [$customer_email];
|
||||
$failures = [];
|
||||
$exception = null;
|
||||
|
||||
@@ -47,21 +47,34 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($mailbox);
|
||||
\App\Misc\Mail::setMailDriver($mailbox, null, $this->conversation);
|
||||
|
||||
// Threads has to be sorted here, if sorted before, they come here in wrong order
|
||||
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
return $item->id;
|
||||
});
|
||||
|
||||
$headers = [];
|
||||
$last_thread = $this->threads->first();
|
||||
|
||||
if (!$last_thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If thread is draft, it means it has been undone
|
||||
if ($last_thread->isDraft()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit conversation history
|
||||
if (config('app.email_user_history') == 'last') {
|
||||
$this->threads = $this->threads->slice(0, 2);
|
||||
}
|
||||
|
||||
if (config('app.email_user_history') == 'none') {
|
||||
$this->threads = $this->threads->slice(0, 1);
|
||||
}
|
||||
|
||||
// All notification for the same conversation has same dummy Message-ID
|
||||
$prev_message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_NOTIFICATION_IN_REPLY.'-'.$this->conversation->id.'-'.md5($this->conversation->id).'@'.$mailbox->getEmailDomain();
|
||||
$headers['In-Reply-To'] = '<'.$prev_message_id.'>';
|
||||
@@ -72,6 +85,15 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
|
||||
foreach ($this->users as $user) {
|
||||
|
||||
// User can ne deleted from DB.
|
||||
if (!isset($user->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($user->isDeleted()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If for one user sending fails the job is marked as failed and retried after some time.
|
||||
// So we have to check if notification email has already been successfully sent to this user.
|
||||
if ($this->attempts() > 1) {
|
||||
@@ -92,7 +114,10 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
// If this is notification on message from customer, set customer as sender name
|
||||
$from_name = '';
|
||||
if ($last_thread->type == Thread::TYPE_CUSTOMER) {
|
||||
$from_name = $last_thread->customer->getFullName();
|
||||
$from_name = '';
|
||||
if ($last_thread->customer) {
|
||||
$from_name = $last_thread->customer->getFullName(true, true);
|
||||
}
|
||||
if ($from_name) {
|
||||
$from_name = $from_name.' '.__('via').' '.$mailbox->name;
|
||||
}
|
||||
@@ -105,6 +130,9 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
// Set user language
|
||||
app()->setLocale($user->getLocale());
|
||||
|
||||
$headers['X-FreeScout-Mail-Type'] = 'user.notification';
|
||||
$headers = \Eventy::filter('jobs.send_reply_to_customer.headers', $headers, $user, $mailbox, $this->conversation, $this->threads, $from);
|
||||
|
||||
$exception = null;
|
||||
|
||||
try {
|
||||
@@ -146,7 +174,13 @@ class SendNotificationToUsers implements ShouldQueue
|
||||
// Retry job with delay.
|
||||
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
|
||||
if ($this->attempts() < $this->tries) {
|
||||
$this->release(3600);
|
||||
if ($this->attempts() == 1) {
|
||||
// Second attempt after 5 min.
|
||||
$this->release(300);
|
||||
} else {
|
||||
// Others - after 1 hour.
|
||||
$this->release(3600);
|
||||
}
|
||||
|
||||
throw $global_exception;
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Mail;
|
||||
use Webklex\IMAP\Client;
|
||||
|
||||
class SendReplyToCustomer implements ShouldQueue
|
||||
{
|
||||
@@ -51,6 +52,7 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
public function handle()
|
||||
{
|
||||
$send_previous_messages = false;
|
||||
$is_forward = false;
|
||||
|
||||
// When forwarding conversation is undone, new conversation is deleted.
|
||||
if (!$this->conversation) {
|
||||
@@ -59,11 +61,16 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
|
||||
$mailbox = $this->conversation->mailbox;
|
||||
|
||||
// Mailbox may be deleted.
|
||||
if (!$mailbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add forwarded conversation replies.
|
||||
if ($this->conversation->threads_count == 1 && count($this->threads) == 1) {
|
||||
$forward_child_thread = $this->threads[0];
|
||||
if ($forward_child_thread->isForwarded() && $forward_child_thread->getForwardParentConversation()) {
|
||||
|
||||
|
||||
// Add replies from original conversation.
|
||||
$forwarded_replies = $forward_child_thread->getForwardParentConversation()->getReplies();
|
||||
$forwarded_replies = $forwarded_replies->sortByDesc(function ($item, $key) {
|
||||
@@ -79,19 +86,22 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
$this->threads = $this->threads->merge($forwarded_replies);
|
||||
$send_previous_messages = true;
|
||||
$is_forward = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Threads has to be sorted here, if sorted before, they come here in wrong order
|
||||
$this->threads = $this->threads->sortByDesc(function ($item, $key) {
|
||||
return $item->created_at;
|
||||
return $item->id;
|
||||
});
|
||||
|
||||
$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
|
||||
@@ -113,13 +123,46 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation history.
|
||||
$email_conv_history = config('app.email_conv_history');
|
||||
|
||||
$meta_conv_history = $this->last_thread->getMeta(Thread::META_CONVERSATION_HISTORY);
|
||||
if (!empty($meta_conv_history)) {
|
||||
$email_conv_history = $meta_conv_history;
|
||||
}
|
||||
|
||||
if ($is_forward && $email_conv_history == 'global') {
|
||||
$email_conv_history = 'full';
|
||||
}
|
||||
|
||||
if ($is_forward && $email_conv_history == 'none') {
|
||||
$email_conv_history = 'full';
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'full') {
|
||||
$send_previous_messages = true;
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'last') {
|
||||
$send_previous_messages = true;
|
||||
$this->threads = $this->threads->slice(0, 2);
|
||||
}
|
||||
|
||||
if ($email_conv_history == 'none') {
|
||||
$send_previous_messages = false;
|
||||
}
|
||||
|
||||
if (!$is_forward) {
|
||||
$send_previous_messages = \Eventy::filter('jobs.send_reply_to_customer.send_previous_messages', $send_previous_messages, $this->last_thread, $this->threads, $this->conversation, $this->customer);
|
||||
}
|
||||
|
||||
// Remove previous messages.
|
||||
if (!$send_previous_messages) {
|
||||
$this->threads = $this->threads->slice(0, 1);
|
||||
}
|
||||
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\App\Misc\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
|
||||
\App\Misc\Mail::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) {
|
||||
@@ -127,7 +170,7 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
$headers['In-Reply-To'] = '<'.$last_customer_thread->message_id.'>';
|
||||
$headers['References'] = '<'.$last_customer_thread->message_id.'>';
|
||||
}
|
||||
|
||||
|
||||
$this->message_id = \App\Misc\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->last_thread->id.'-'.md5($this->last_thread->id).'@'.$mailbox->getEmailDomain();
|
||||
$headers['Message-ID'] = $this->message_id;
|
||||
|
||||
@@ -168,11 +211,15 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
// $bcc_array = [];
|
||||
// }
|
||||
|
||||
$headers['X-FreeScout-Mail-Type'] = 'customer.message';
|
||||
|
||||
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox);
|
||||
|
||||
try {
|
||||
Mail::to($to)
|
||||
->cc($cc_array)
|
||||
->bcc($bcc_array)
|
||||
->send(new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox));
|
||||
->send($reply_mail);
|
||||
} catch (\Exception $e) {
|
||||
// We come here in case SMTP server unavailable for example
|
||||
if ($this->attempts() == 1) {
|
||||
@@ -194,15 +241,23 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
$this->saveToSendLog($e->getMessage());
|
||||
}
|
||||
|
||||
$error_message = $e->getMessage();
|
||||
|
||||
// Retry job with delay.
|
||||
// https://stackoverflow.com/questions/35258175/how-can-i-create-delays-between-failed-queued-job-attempts-in-laravel
|
||||
if ($this->attempts() < $this->tries) {
|
||||
$this->release(3600);
|
||||
if ($this->attempts() < $this->tries && !preg_match("/".config("app.no_retry_mail_errors")."/i", $error_message)) {
|
||||
if ($this->attempts() == 1) {
|
||||
// Second attempt after 5 min.
|
||||
$this->release(300);
|
||||
} else {
|
||||
// Others - after 1 hour.
|
||||
$this->release(3600);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
} else {
|
||||
$this->last_thread->send_status = SendLog::STATUS_SEND_ERROR;
|
||||
$this->last_thread->updateSendStatusData(['msg' => $e->getMessage()]);
|
||||
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
|
||||
$this->last_thread->save();
|
||||
|
||||
// This executes $this->failed().
|
||||
@@ -212,6 +267,53 @@ class SendReplyToCustomer implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
$imap_sent_folder = $mailbox->imap_sent_folder;
|
||||
if ($imap_sent_folder) {
|
||||
try {
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
$client->connect();
|
||||
|
||||
$envelope['from'] = $mailbox->getMailFrom(null, $this->conversation)['address'];
|
||||
$envelope['to'] = $this->customer_email;
|
||||
$envelope['subject'] = 'Re: ' . $this->conversation->subject;
|
||||
|
||||
// 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.'>'
|
||||
];
|
||||
}
|
||||
|
||||
$part1['type'] = TYPETEXT;
|
||||
$part1['subtype'] = 'html';
|
||||
$part1['contents.data'] = $reply_mail->render();
|
||||
|
||||
try {
|
||||
// getFolder does not work if sent folder has spaces.
|
||||
$folder = $client->getFolder($imap_sent_folder);
|
||||
if ($folder) {
|
||||
if (get_class($client) == 'Webklex\PHPIMAP\Client') {
|
||||
$folder->appendMessage(imap_mail_compose($envelope, [$part1]), ['Seen'], now()->format('d-M-Y H:i:s O'));
|
||||
} else {
|
||||
$folder->appendMessage(imap_mail_compose($envelope, [$part1]), '\Seen', now()->format('d-M-Y H:i:s O'));
|
||||
}
|
||||
} else {
|
||||
\Log::error('Could not save outgoing reply to the IMAP folder (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.' - ');
|
||||
//$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.' - ');
|
||||
}
|
||||
}
|
||||
|
||||
// In message_id we are storing Message-ID of the incoming email which created the thread
|
||||
// Outcoming message_id can be generated for each thread by thread->id
|
||||
// $this->last_thread->message_id = $message_id;
|
||||
|
||||
25
app/Listeners/ProcessSwiftMessage.php
Normal file
25
app/Listeners/ProcessSwiftMessage.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
class ProcessSwiftMessage
|
||||
{
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
return \Eventy::filter('mail.process_swift_message', true, $event->message);
|
||||
}
|
||||
}
|
||||
30
app/Listeners/RefreshConversations.php
Normal file
30
app/Listeners/RefreshConversations.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Conversation;
|
||||
|
||||
class RefreshConversations
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param $event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
Conversation::refreshConversations($event->conversation, $event->thread ?? $event->last_thread);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Mailbox;
|
||||
use App\SendLog;
|
||||
|
||||
class SendAutoReply
|
||||
{
|
||||
const CHECK_PERIOD = 180; // min
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
@@ -23,7 +27,7 @@ class SendAutoReply
|
||||
{
|
||||
$conversation = $event->conversation;
|
||||
|
||||
if (/*!$conversation->imported &&*/ $conversation->mailbox->auto_reply_enabled) {
|
||||
if (!$conversation->imported && $conversation->mailbox->auto_reply_enabled) {
|
||||
$thread = $conversation->threads()->first();
|
||||
|
||||
// Do not send auto reply to auto responders.
|
||||
@@ -35,20 +39,52 @@ class SendAutoReply
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not send auto replies to spam messages.
|
||||
if ($conversation->status == Conversation::STATUS_SPAM) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\Eventy::filter('autoreply.should_send', true, $conversation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We can not send auto reply to incoming bounce messages, as it will lead to the infinite loop:
|
||||
// application will be sending auto replies and mail server will be sending bounce messages to auto replies.
|
||||
// Bounce detection can not be 100% reliable.
|
||||
// So to prevent infinite loop, we are checking number of auto replies sent to the customer recently.
|
||||
$created_at = \Illuminate\Support\Carbon::now()->subMinutes(10);
|
||||
$created_at = \Illuminate\Support\Carbon::now()->subMinutes(self::CHECK_PERIOD);
|
||||
|
||||
$auto_replies_sent = SendLog::where('customer_id', $conversation->customer_id)
|
||||
->where('mail_type', SendLog::MAIL_TYPE_AUTO_REPLY)
|
||||
->where('created_at', '>', $created_at)
|
||||
->count();
|
||||
|
||||
if ($auto_replies_sent >= 3) {
|
||||
if ($auto_replies_sent >= 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($auto_replies_sent >= 2) {
|
||||
// Find conversations from this customer
|
||||
$prev_conversations = Conversation::select('subject', 'id')
|
||||
->where('customer_id', $conversation->customer_id)
|
||||
->where('created_at', '>', $created_at)
|
||||
->get();
|
||||
|
||||
foreach ($prev_conversations as $prev_conv) {
|
||||
if ($prev_conv->subject == $conversation->subject && $prev_conv->id != $conversation->id) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not send autoreplies to own mailboxes.
|
||||
if ($conversation->customer_email) {
|
||||
$is_internal_email = Mailbox::where('email', $conversation->customer_email)->exists();
|
||||
if ($is_internal_email) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 24h limit has been disabled: https://github.com/freescout-helpdesk/freescout/pull/95
|
||||
// Send auto reply once in 24h
|
||||
/*$created_at = \Illuminate\Support\Carbon::now()->subDays(1);
|
||||
|
||||
@@ -62,11 +62,12 @@ class SendNotificationToUsers
|
||||
if (empty($event->conversation) || !$event_type) {
|
||||
return;
|
||||
}
|
||||
$conversation = $event->conversation;
|
||||
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
//if (!$conversation->imported) {
|
||||
// Ignore imported threads.
|
||||
if (!empty($event->thread) && $event->thread->imported) {
|
||||
return;
|
||||
}
|
||||
$conversation = $event->conversation;
|
||||
|
||||
// Using the last argument you can make event to be processed immediately
|
||||
Subscription::registerEvent($event_type, $conversation, $caused_by_user_id/*, true*/);
|
||||
|
||||
@@ -28,11 +28,26 @@ class SendReplyToCustomer
|
||||
return;
|
||||
}
|
||||
|
||||
$replies = $conversation->getReplies();
|
||||
|
||||
// Ignore imported messages.
|
||||
if ($replies && $replies->first() && $replies->first()->imported) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat conversation.
|
||||
if ($conversation->isChat()) {
|
||||
\Helper::backgroundAction('chat_conversation.send_reply', [$conversation, $replies, $conversation->customer], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
return;
|
||||
}
|
||||
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
//if (!$conversation->imported) {
|
||||
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $conversation->getReplies(), $conversation->customer)
|
||||
->delay(now()->addSeconds(Conversation::UNDO_TIMOUT))
|
||||
$delay = \Eventy::filter('conversation.send_reply_to_customer_delay', now()->addSeconds(Conversation::UNDO_TIMOUT), $conversation, $replies);
|
||||
|
||||
\App\Jobs\SendReplyToCustomer::dispatch($conversation, $replies, $conversation->customer)
|
||||
->delay($delay)
|
||||
->onQueue('emails');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,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)
|
||||
|
||||
@@ -81,6 +81,7 @@ class ReplyToCustomer extends Mailable
|
||||
if (count($this->threads) > 1) {
|
||||
$subject = 'Re: '.$subject;
|
||||
}
|
||||
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation);
|
||||
|
||||
// from($this->from) Sets only email, name stays empty.
|
||||
// So we set from in Mail::setMailDriver
|
||||
|
||||
@@ -23,6 +23,13 @@ class Test extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$this->withSwiftMessage(function ($swiftmessage) {
|
||||
$headers = $swiftmessage->getHeaders();
|
||||
$headers->addTextHeader('X-FreeScout-Mail-Type', 'test.mailbox');
|
||||
|
||||
return $swiftmessage;
|
||||
});
|
||||
|
||||
$message = $this->subject(__(':app_name Test Email', ['app_name' => \Config::get('app.name')]));
|
||||
if ($this->mailbox) {
|
||||
$message->view('emails/user/test', ['mailbox' => $this->mailbox]);
|
||||
|
||||
@@ -27,7 +27,7 @@ class UserInvite extends Mailable
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$message = $this->subject(__('Welcome to :company_name!', ['company_name' => Option::get('company_name')]))
|
||||
$message = $this->subject(__('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]))
|
||||
->view('emails/user/user_invite')
|
||||
->text('emails/user/user_invite_text');
|
||||
|
||||
|
||||
348
app/Mailbox.php
348
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;
|
||||
@@ -11,7 +13,7 @@ class Mailbox extends Model
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
|
||||
/**
|
||||
* From Name: name that will appear in the From field when a customer views your email.
|
||||
*/
|
||||
@@ -91,6 +93,28 @@ class Mailbox extends Model
|
||||
const RATINGS_PLACEMENT_ABOVE = 1;
|
||||
const RATINGS_PLACEMENT_BELOW = 2;
|
||||
|
||||
/**
|
||||
* Access permissions.
|
||||
*/
|
||||
const ACCESS_PERM_EDIT = 'edit';
|
||||
const ACCESS_PERM_PERMISSIONS = 'perm';
|
||||
const ACCESS_PERM_AUTO_REPLIES = 'auto';
|
||||
const ACCESS_PERM_SIGNATURE = 'sig';
|
||||
|
||||
public static $access_permissions = [
|
||||
self::ACCESS_PERM_EDIT,
|
||||
self::ACCESS_PERM_PERMISSIONS,
|
||||
self::ACCESS_PERM_AUTO_REPLIES,
|
||||
self::ACCESS_PERM_SIGNATURE,
|
||||
];
|
||||
|
||||
public static $access_routes = [
|
||||
self::ACCESS_PERM_EDIT => 'mailboxes.update',
|
||||
self::ACCESS_PERM_PERMISSIONS => 'mailboxes.permissions',
|
||||
self::ACCESS_PERM_AUTO_REPLIES => 'mailboxes.auto_reply',
|
||||
self::ACCESS_PERM_SIGNATURE => 'mailboxes.update',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default signature set when mailbox created.
|
||||
*/
|
||||
@@ -104,20 +128,24 @@ class Mailbox extends Model
|
||||
'signature' => self::DEFAULT_SIGNATURE,
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'meta' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Attributes fillable using fill() method.
|
||||
*
|
||||
* @var [type]
|
||||
*/
|
||||
protected $fillable = ['name', 'slug', 'email', 'aliases', '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'];
|
||||
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 static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
self::created(function (Mailbox $model) {
|
||||
$model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
|
||||
});
|
||||
// self::created(function (Mailbox $model) {
|
||||
// $model->slug = strtolower(substr(md5(Hash::make($model->id)), 0, 16));
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +153,11 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function setInPasswordAttribute($value)
|
||||
{
|
||||
$this->attributes['in_password'] = encrypt($value);
|
||||
if ($value != '') {
|
||||
$this->attributes['in_password'] = encrypt($value);
|
||||
} else {
|
||||
$this->attributes['in_password'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +173,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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +211,16 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany('App\User')->as('settings')->withPivot('after_send');
|
||||
return $this->belongsToMany('App\User');
|
||||
}
|
||||
|
||||
public function usersWithSettings()
|
||||
{
|
||||
return $this->belongsToMany('App\User')->as('settings')
|
||||
->withPivot('after_send')
|
||||
->withPivot('hide')
|
||||
->withPivot('mute')
|
||||
->withPivot('access');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,11 +284,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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,6 +325,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])
|
||||
@@ -297,7 +372,7 @@ class Mailbox extends Model
|
||||
->orderBy('type')
|
||||
->get();
|
||||
|
||||
return \Eventy::filter('mailbox.folders', $folders);
|
||||
return \Eventy::filter('mailbox.folders', $folders, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,6 +408,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 {
|
||||
@@ -363,22 +444,58 @@ class Mailbox extends Model
|
||||
{
|
||||
$admins = User::where('role', User::ROLE_ADMIN)->select($fields)->remember(\Helper::cacheTime($cache))->get();
|
||||
|
||||
$users = $this->users()->select($fields)->rememberForever()->get()->merge($admins)->unique();
|
||||
$users = $this->users()->select($fields)->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
|
||||
|
||||
// Exclude deleted users (better to do it in PHP).
|
||||
foreach ($users as $i => $user) {
|
||||
if ($user->isDeleted()) {
|
||||
if (!$user->isActive()) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by full name
|
||||
if ($sort) {
|
||||
$users = $users->sortBy(function ($value, $key) {
|
||||
return $value->getFullName();
|
||||
}, SORT_STRING | SORT_FLAG_CASE);
|
||||
$users = User::sortUsers($users);
|
||||
}
|
||||
|
||||
$users = \Eventy::filter('mailbox.users_having_access', $users, $this, $cache, $fields, $sort);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function usersAssignable($cache = true)
|
||||
{
|
||||
// Exclude hidden admins.
|
||||
$mailbox_id = $this->id;
|
||||
$admins = User::select(['users.*', 'mailbox_user.hide'])
|
||||
->leftJoin('mailbox_user', function ($join) use ($mailbox_id) {
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
$join->where('mailbox_user.mailbox_id', $mailbox_id);
|
||||
})
|
||||
->where('role', User::ROLE_ADMIN)
|
||||
->remember(\Helper::cacheTime($cache))
|
||||
->get();
|
||||
|
||||
$users = $this->users()->select('users.*')->remember(\Helper::cacheTime($cache))->get()->merge($admins)->unique();
|
||||
|
||||
foreach ($users as $i => $user) {
|
||||
if (!empty($user->hide)) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude deleted users (better to do it in PHP).
|
||||
foreach ($users as $i => $user) {
|
||||
if (!$user->isActive()) {
|
||||
$users->forget($i);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by full name
|
||||
$users = User::sortUsers($users);
|
||||
|
||||
$users = \Eventy::filter('mailbox.users_assignable', $users, $this, $cache);
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
@@ -403,9 +520,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();
|
||||
@@ -416,10 +540,11 @@ 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;
|
||||
@@ -430,7 +555,7 @@ class Mailbox extends Model
|
||||
$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 ) ];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -490,7 +615,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -498,19 +648,40 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getUserSettings($user_id)
|
||||
{
|
||||
$mailbox_user = $this->users()->where('users.id', $user_id)->first();
|
||||
$mailbox_user = $this->usersWithSettings()->where('users.id', $user_id)->first();
|
||||
if ($mailbox_user) {
|
||||
return $mailbox_user->settings;
|
||||
} else {
|
||||
// Admin may have no record in mailbox_user table
|
||||
// Create dummy object with default parameters
|
||||
$settings = new \StdClass();
|
||||
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
|
||||
|
||||
return $settings;
|
||||
return self::getDummySettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create dummy object with default parameters
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function getDummySettings()
|
||||
{
|
||||
$settings = new \StdClass();
|
||||
$settings->after_send = MailboxUser::AFTER_SEND_NEXT;
|
||||
$settings->hide = false;
|
||||
$settings->mute = false;
|
||||
$settings->access = [];
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function fetchUserSettings($user_id)
|
||||
{
|
||||
$settings = $this->getUserSettings($user_id);
|
||||
|
||||
$this->after_send = $settings->after_send;
|
||||
$this->hide = $settings->hide;
|
||||
$this->mute = $settings->mute;
|
||||
$this->access = $settings->access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main email and aliases.
|
||||
*
|
||||
@@ -583,7 +754,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -615,7 +786,12 @@ class Mailbox extends Model
|
||||
*/
|
||||
public function getInImapFolders()
|
||||
{
|
||||
return \Helper::jsonToArray($this->in_imap_folders);
|
||||
$in_imap_folders = \Helper::jsonToArray($this->in_imap_folders);
|
||||
if (count($in_imap_folders)) {
|
||||
return $in_imap_folders;
|
||||
} else {
|
||||
return ["INBOX"];
|
||||
}
|
||||
}
|
||||
|
||||
public function outPasswordSafe()
|
||||
@@ -627,4 +803,112 @@ class Mailbox extends Model
|
||||
{
|
||||
return \Helper::safePassword($this->in_password);
|
||||
}
|
||||
|
||||
public function getReplySeparator()
|
||||
{
|
||||
return $this->before_reply ?: \MailHelper::REPLY_SEPARATOR_TEXT;
|
||||
}
|
||||
|
||||
public static function findOrFailWithSettings($id, $user_id)
|
||||
{
|
||||
return Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
|
||||
->where('mailboxes.id', $id)
|
||||
->leftJoin('mailbox_user', function ($join) use ($user_id) {
|
||||
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
|
||||
$join->where('mailbox_user.user_id', $user_id);
|
||||
})->firstOrFail();
|
||||
}
|
||||
|
||||
/*public static function getUserSettings($mailbox_id, $user_id)
|
||||
{
|
||||
return MailboxUser::where('mailbox_id', $mailbox_id)
|
||||
->where('user_id', $user_id)
|
||||
->first();
|
||||
}*/
|
||||
|
||||
public static function getAccessPermissionName($perm)
|
||||
{
|
||||
$access_permissions = [
|
||||
self::ACCESS_PERM_EDIT => __('Edit Mailbox'),
|
||||
self::ACCESS_PERM_PERMISSIONS => __('Permissions'),
|
||||
self::ACCESS_PERM_AUTO_REPLIES => __('Auto Replies'),
|
||||
self::ACCESS_PERM_SIGNATURE => __('Email Signature'),
|
||||
];
|
||||
$access_permissions = \Eventy::filter('mailbox.access_permissions_list', $access_permissions);
|
||||
|
||||
return $access_permissions[$perm] ?? '';
|
||||
}
|
||||
|
||||
public static function getAccessPermissionRoute($perm)
|
||||
{
|
||||
$route = self::$access_routes[$perm] ?? '';
|
||||
$route = \Eventy::filter('mailbox.access_permissions_route', $route, $perm);
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
public function getMeta($key, $default = null)
|
||||
{
|
||||
if (isset($this->meta[$key])) {
|
||||
return $this->meta[$key];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
public function setMeta($key, $value)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
$meta[$key] = $value;
|
||||
$this->meta = $meta;
|
||||
}
|
||||
|
||||
public function setMetaParam($param, $value, $save = false)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
$meta[$param] = $value;
|
||||
$this->meta = $meta;
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeMetaParam($param, $save = false)
|
||||
{
|
||||
$meta = $this->meta;
|
||||
if (isset($meta[$param])) {
|
||||
unset($meta[$param]);
|
||||
}
|
||||
$this->meta = $meta;
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a user with specified email.
|
||||
*/
|
||||
public static function userEmailExists($email)
|
||||
{
|
||||
$email = Email::sanitizeEmail($email);
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if ($user) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function oauthEnabled()
|
||||
{
|
||||
return !empty($this->meta['oauth']['provider']);
|
||||
}
|
||||
|
||||
public function oauthGetParam($param)
|
||||
{
|
||||
return $this->meta['oauth'][$param] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,25 @@
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Watson\Rememberable\Rememberable;
|
||||
|
||||
class MailboxUser extends Model
|
||||
{
|
||||
use Rememberable;
|
||||
// This is obligatory.
|
||||
public $rememberCacheDriver = 'array';
|
||||
|
||||
// Action after sending a message
|
||||
const AFTER_SEND_STAY = 1;
|
||||
const AFTER_SEND_NEXT = 2;
|
||||
const AFTER_SEND_FOLDER = 3;
|
||||
|
||||
protected $table = 'mailbox_user';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
// Does not work as we receive it via pivot
|
||||
// protected $casts = [
|
||||
// 'access' => 'array',
|
||||
// ];
|
||||
}
|
||||
|
||||
@@ -25,6 +25,20 @@ class Helper
|
||||
*/
|
||||
const QUEUE_DEFAULT = 'default';
|
||||
|
||||
/**
|
||||
* Stores list of global entities (for caching).
|
||||
*/
|
||||
public static $global_entities = [];
|
||||
|
||||
/**
|
||||
* Files with such extensions are being renamed on upload.
|
||||
*/
|
||||
public static $restricted_extensions = [
|
||||
'php.*',
|
||||
'sh',
|
||||
'pl',
|
||||
];
|
||||
|
||||
/**
|
||||
* Menu structure used to display active menu item.
|
||||
* Array are mnemonic names, strings - route names.
|
||||
@@ -38,7 +52,7 @@ class Helper
|
||||
'conversations.view',
|
||||
'conversations.create',
|
||||
'conversations.draft',
|
||||
'conversations.search',
|
||||
//'conversations.search',
|
||||
],
|
||||
'manage' => [
|
||||
'settings' => 'settings',
|
||||
@@ -462,6 +476,16 @@ class Helper
|
||||
}
|
||||
}
|
||||
|
||||
public static function setGlobalEntity($name, $entity)
|
||||
{
|
||||
self::$global_entities[$name] = $entity;
|
||||
}
|
||||
|
||||
public static function getGlobalEntity($name)
|
||||
{
|
||||
return self::$global_entities[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove from text all tags, double spaces, etc.
|
||||
*/
|
||||
@@ -469,7 +493,7 @@ class Helper
|
||||
{
|
||||
// Remove all kinds of spaces after tags.
|
||||
// https://stackoverflow.com/questions/3230623/filter-all-types-of-whitespace-in-php
|
||||
$text = preg_replace("/^(.*)>[\r\n]*\s+/mu", '$1>', $text);
|
||||
$text = preg_replace("/^(.*)>[\r\n]*\s+/mu", '$1>', $text ?? '');
|
||||
|
||||
// Remove <script> and <style> blocks.
|
||||
$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text);
|
||||
@@ -552,7 +576,7 @@ class Helper
|
||||
}
|
||||
} else {
|
||||
if ($current_route == $secondary_routes) {
|
||||
return $primary_name == $menu_item_name;
|
||||
return $primary_name == $menu_item_name || $menu_item_name == $secondary_routes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,8 +606,14 @@ class Helper
|
||||
|
||||
if (preg_match('/png/i', $mime_type)) {
|
||||
$src = imagecreatefrompng($file);
|
||||
|
||||
$kek = imagecolorallocate($src, 255, 255, 255);
|
||||
imagefill($src, 0, 0, $kek);
|
||||
} elseif (preg_match('/gif/i', $mime_type)) {
|
||||
$src = imagecreatefromgif($file);
|
||||
|
||||
$kek = imagecolorallocate($src, 255, 255, 255);
|
||||
imagefill($src, 0, 0, $kek);
|
||||
} elseif (preg_match('/bmp/i', $mime_type)) {
|
||||
$src = imagecreatefrombmp($file);
|
||||
} else {
|
||||
@@ -609,10 +639,11 @@ class Helper
|
||||
// Resize and crop
|
||||
imagecopyresampled($thumb,
|
||||
$src,
|
||||
0 - ($new_width - $thumb_width) / 2, // Center the image horizontally
|
||||
0 - ($new_height - $thumb_height) / 2, // Center the image vertically
|
||||
ceil(0 - ($new_width - $thumb_width) / 2), // Center the image horizontally
|
||||
ceil(0 - ($new_height - $thumb_height) / 2), // Center the image vertically
|
||||
0, 0,
|
||||
$new_width, $new_height,
|
||||
ceil($new_width),
|
||||
ceil($new_height),
|
||||
$width, $height);
|
||||
imagedestroy($src);
|
||||
|
||||
@@ -645,36 +676,39 @@ class Helper
|
||||
* Create zip archive.
|
||||
* Source example: public/files/*
|
||||
* File name example: test.zip.
|
||||
* storage_path without app/
|
||||
*/
|
||||
public static function createZipArchive($source, $file_name, $folder = '')
|
||||
public static function createZipArchive($source, $file_name, $folder = '', $storage_file_path = '')
|
||||
{
|
||||
if (!$source || !$file_name) {
|
||||
return false;
|
||||
}
|
||||
$files = glob($source);
|
||||
|
||||
//$dest_folder = storage_path().DIRECTORY_SEPARATOR.'app/zipper';
|
||||
// if (!file_exists($dest_folder)) {
|
||||
// $mkdir_result = File::makeDirectory($dest_folder, 0755);
|
||||
// if (!$mkdir_result) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
$storage_path = 'app/zipper'.DIRECTORY_SEPARATOR.$file_name;
|
||||
$dest_path = storage_path().DIRECTORY_SEPARATOR.$storage_path;
|
||||
if (!$storage_file_path) {
|
||||
$storage_file_path = 'zipper'.DIRECTORY_SEPARATOR.$file_name;
|
||||
} else {
|
||||
// if (!self::getPrivateStorage()->exists($storage_path)) {
|
||||
// self::getPrivateStorage()->makeDirectory($storage_path);
|
||||
// }
|
||||
}
|
||||
$dest_path = storage_path().DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.$storage_file_path;
|
||||
|
||||
// If file exists it has to be deleted, otherwise Zipper will add file to the existing archive
|
||||
// By some reason \Storage not finding $storage_path file
|
||||
// todo: use \Storage
|
||||
if (\File::exists($dest_path)) {
|
||||
\File::delete($dest_path);
|
||||
if (self::getPrivateStorage()->exists($storage_file_path)) {
|
||||
self::getPrivateStorage()->delete($storage_file_path);
|
||||
}
|
||||
|
||||
\Chumper\Zipper\Facades\Zipper::make($dest_path)->folder($folder)->add($files)->close();
|
||||
|
||||
return $dest_path;
|
||||
}
|
||||
|
||||
public static function getPrivateStorage()
|
||||
{
|
||||
return \Storage::disk('private');
|
||||
}
|
||||
|
||||
public static function formatException($e)
|
||||
{
|
||||
return 'Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')';
|
||||
@@ -703,7 +737,7 @@ class Helper
|
||||
public static function downloadRemoteFile($url, $destinationFilePath)
|
||||
{
|
||||
$client = new \GuzzleHttp\Client();
|
||||
|
||||
|
||||
$client->request('GET', $url, [
|
||||
'sink' => $destinationFilePath,
|
||||
'connect_timeout' => 7,
|
||||
@@ -802,6 +836,13 @@ class Helper
|
||||
}
|
||||
}
|
||||
|
||||
public static function setLocale($locale)
|
||||
{
|
||||
if (in_array($locale, config('app.locales'))) {
|
||||
app()->setLocale($locale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale's data.
|
||||
*
|
||||
@@ -847,9 +888,13 @@ class Helper
|
||||
$env_path = app()->environmentFilePath();
|
||||
$contents = file_get_contents($env_path);
|
||||
|
||||
// Maybe validate value and add quotes.
|
||||
// str_contains($key, '=')
|
||||
// !preg_match('/^[a-zA-Z_]+$/', $key)
|
||||
if (strstr($value, '"')) {
|
||||
// Escape quotes.
|
||||
$value = '"'.str_replace('"', '\"', $value).'"';
|
||||
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $value) && $value !== '') {
|
||||
// Add quotes.
|
||||
$value = '"'.$value.'"';
|
||||
}
|
||||
|
||||
$old_value = '';
|
||||
// Match the given key at the beginning of a line
|
||||
@@ -921,6 +966,8 @@ class Helper
|
||||
*/
|
||||
public static function backgroundAction($action, $params, $delay = 0)
|
||||
{
|
||||
$delay = \Eventy::filter('backgound_action.dispatch_delay', $delay, $action, $params);
|
||||
|
||||
$job = \App\Jobs\TriggerAction::dispatch($action, $params);
|
||||
if ($delay) {
|
||||
$job->delay($delay);
|
||||
@@ -933,12 +980,17 @@ class Helper
|
||||
*
|
||||
* @param [type] $text [description]
|
||||
*/
|
||||
public static function htmlToText($text)
|
||||
public static function htmlToText($text, $embed_images = false, $options = ['width' => 0])
|
||||
{
|
||||
// Process blockquotes.
|
||||
$text = str_ireplace('<blockquote>', '<div>', $text);
|
||||
$text = str_ireplace('</blockquote>', '</div>', $text);
|
||||
return (new \Html2Text\Html2Text($text))->getText();
|
||||
|
||||
if ($embed_images) {
|
||||
// Replace embedded images with their urls.
|
||||
$text = preg_replace( '/<img\b[^>]*src=\"([^>"]+)\"[^>]*>/i', "<div>$1</div>", $text);
|
||||
}
|
||||
return (new \Html2Text\Html2Text($text, $options))->getText();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -994,7 +1046,7 @@ class Helper
|
||||
$subdirectory = $_SERVER['SCRIPT_NAME'];
|
||||
} elseif (basename($_SERVER['PHP_SELF']) === $filename) {
|
||||
$subdirectory = $_SERVER['PHP_SELF'];
|
||||
} elseif (basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
|
||||
} elseif (array_key_exists('ORIG_SCRIPT_NAME', $_SERVER) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
|
||||
$subdirectory = $_SERVER['ORIG_SCRIPT_NAME']; // 1and1 shared hosting compatibility
|
||||
} else {
|
||||
// Backtrack up the script_filename to find the portion matching
|
||||
@@ -1014,6 +1066,10 @@ class Helper
|
||||
}
|
||||
}
|
||||
|
||||
if ($subdirectory === null) {
|
||||
$subdirectory = '';
|
||||
}
|
||||
|
||||
$subdirectory = str_replace('public/index.php', '', $subdirectory);
|
||||
$subdirectory = str_replace('index.php', '', $subdirectory);
|
||||
|
||||
@@ -1187,6 +1243,10 @@ class Helper
|
||||
|
||||
public static function purifyHtml($html)
|
||||
{
|
||||
if (!$html) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html = \Purifier::clean($html);
|
||||
|
||||
// Remove all kinds of spaces after tags
|
||||
@@ -1201,7 +1261,7 @@ class Helper
|
||||
*/
|
||||
public static function safePassword($password)
|
||||
{
|
||||
return str_repeat("*", mb_strlen($password));
|
||||
return str_repeat("*", mb_strlen($password ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1264,10 +1324,10 @@ class Helper
|
||||
*/
|
||||
public static function getWorkerIdentifier()
|
||||
{
|
||||
return md5(config('app.key'));
|
||||
return md5(config('app.key') ?? '');
|
||||
}
|
||||
|
||||
public static function uploadFile($file, $allowed_exts = [], $allowed_mimes = [])
|
||||
public static function uploadFile($file, $allowed_exts = [], $allowed_mimes = [], $sanitize_file_name = true)
|
||||
{
|
||||
$ext = strtolower($file->getClientOriginalExtension());
|
||||
|
||||
@@ -1283,11 +1343,15 @@ class Helper
|
||||
throw new \Exception(__('Unsupported file type'), 1);
|
||||
}
|
||||
}
|
||||
$name = \Str::random(25).'.'.$ext;
|
||||
$file_name = \Str::random(25).'.'.$ext;
|
||||
|
||||
$file->storeAs('uploads', $name);
|
||||
if ($sanitize_file_name) {
|
||||
$file_name = \Helper::sanitizeUploadedFileName($file_name);
|
||||
}
|
||||
|
||||
return self::uploadedFilePath($name);
|
||||
$file->storeAs('uploads', $file_name);
|
||||
|
||||
return self::uploadedFilePath($file_name);
|
||||
}
|
||||
|
||||
public static function uploadedFileRemove($name)
|
||||
@@ -1333,4 +1397,225 @@ class Helper
|
||||
|
||||
\Session::flash('flashes_floating', $flashes);
|
||||
}
|
||||
|
||||
public static function isMySql()
|
||||
{
|
||||
return \DB::connection()->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'mysql';
|
||||
}
|
||||
|
||||
public static function isPgSql()
|
||||
{
|
||||
return \DB::connection()->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME) == 'pgsql';
|
||||
}
|
||||
|
||||
public static function humanFileSize($size, $unit="")
|
||||
{
|
||||
if ((!$unit && $size >= 1<<30) || $unit == "GB") {
|
||||
return number_format($size/(1<<30),2)."GB";
|
||||
}
|
||||
if ((!$unit && $size >= 1<<20) || $unit == "MB") {
|
||||
return number_format($size/(1<<20),2)."MB";
|
||||
}
|
||||
//if ((!$unit && $size >= 1<<10) || $unit == "KB") {
|
||||
return number_format($size/(1<<10),2)."KB";
|
||||
// }
|
||||
// return number_format($size)." bytes";
|
||||
}
|
||||
|
||||
public static function isPrint()
|
||||
{
|
||||
return (bool)app('request')->input('print');
|
||||
}
|
||||
|
||||
public static function isDev()
|
||||
{
|
||||
return config('app.env') != 'production';
|
||||
}
|
||||
|
||||
public static function substrUnicode($str, $s, $l = null)
|
||||
{
|
||||
return join("", array_slice(preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY), $s, $l));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable sql_require_primary_key option to avoid errors when migrating.
|
||||
* Only for MySQL.
|
||||
*/
|
||||
public static function disableSqlRequirePrimaryKey()
|
||||
{
|
||||
if (!self::isMySql()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
\DB::statement('SET SESSION sql_require_primary_key=0');
|
||||
} catch (\Exception $e) {
|
||||
// General error: 1193 Unknown system variable 'sql_require_primary_key'.
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
public static function downloadRemoteFileAsTmp($uri)
|
||||
{
|
||||
try {
|
||||
$headers = get_headers($uri);
|
||||
|
||||
if (!preg_match("/200/", $headers[0])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$contents = file_get_contents($uri);
|
||||
|
||||
if (!$contents) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$temp_file = tempnam(sys_get_temp_dir(), 'fs');
|
||||
|
||||
\File::put($temp_file, $contents);
|
||||
|
||||
return $temp_file;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
\Helper::logException($e, 'Error downloading a remote file ('.$uri.'): ');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function downloadRemoteFileAsTmpFile($uri)
|
||||
{
|
||||
$file_path = self::downloadRemoteFileAsTmp($uri);
|
||||
if ($file_path) {
|
||||
return new \Illuminate\Http\UploadedFile(
|
||||
$file_path, basename($file_path),
|
||||
null, null, true
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function sanitizeUploadedFileName($file_name)
|
||||
{
|
||||
// Check extension.
|
||||
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', strtolower($extension))) {
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = $file_name.'_';
|
||||
}
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
public static function remoteFileName($file_url)
|
||||
{
|
||||
return preg_replace("/\?.*/", '', basename($file_url));
|
||||
}
|
||||
|
||||
public static function binaryDataMimeType($data)
|
||||
{
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
return $finfo->buffer($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* https://php.watch/versions/8.0/deprecated-reflectionparameter-methods
|
||||
*/
|
||||
public static function getClass($param)
|
||||
{
|
||||
return $param->getType() && !$param->getType()->isBuiltin() ? new \ReflectionClass(method_exists($param->getType(), 'getName') ? $param->getType()->getName() : $param->getClass()->name) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://php.watch/versions/8.0/deprecated-reflectionparameter-methods
|
||||
*/
|
||||
public static function getClassName($param)
|
||||
{
|
||||
return $param->getType() && !$param->getType()->isBuiltin() ? method_exists($param->getType(), 'getName') ? $param->getType()->getName() : $param->getClass()->name : null;
|
||||
}
|
||||
|
||||
public static function getWebCronHash()
|
||||
{
|
||||
return md5(config('app.key').'web_cron_hash');
|
||||
}
|
||||
|
||||
public static function fixProtocol($url)
|
||||
{
|
||||
if (parse_url(config('app.url'), PHP_URL_SCHEME) == 'http' && parse_url($url, PHP_URL_SCHEME) != 'http') {
|
||||
return str_replace('https://', 'http://', $url);
|
||||
}
|
||||
|
||||
if (parse_url(config('app.url'), PHP_URL_SCHEME) == 'https' && parse_url($url, PHP_URL_SCHEME) != 'https') {
|
||||
return str_replace('http://', 'https://', $url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix and parse date to Carbon.
|
||||
*/
|
||||
public static function parseDateToCarbon($date)
|
||||
{
|
||||
if (preg_match('/\+0580/', $date)) {
|
||||
$date = str_replace('+0580', '+0530', $date);
|
||||
}
|
||||
$date = trim(rtrim($date));
|
||||
$date = preg_replace('/[<>]/', '', $date);
|
||||
$date = str_replace('_', ' ', $date);
|
||||
try {
|
||||
return Carbon::parse($date);
|
||||
} catch (\Exception $e) {
|
||||
switch (true) {
|
||||
case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0:
|
||||
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0:
|
||||
$date .= 'C';
|
||||
break;
|
||||
case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0:
|
||||
case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0:
|
||||
case preg_match('/([A-Z]{2,3}\, \ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0:
|
||||
case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{2,4}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2}\ [A-Z]{2}\ \-[0-9]{2}\:[0-9]{2}\ \([A-Z]{2,3}\ \-[0-9]{2}:[0-9]{2}\))+$/i', $date) > 0:
|
||||
$array = explode('(', $date);
|
||||
$array = array_reverse($array);
|
||||
$date = trim(array_pop($array));
|
||||
break;
|
||||
}
|
||||
try {
|
||||
return Carbon::parse($date);
|
||||
} catch (\Exception $_e) {
|
||||
return Carbon::now();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function urlHome()
|
||||
{
|
||||
return \Config::get('app.url');
|
||||
// $url = rtrim($url, "/");
|
||||
// return $url.'/home';
|
||||
}
|
||||
|
||||
/**
|
||||
* Request::url() may return URL with incorrect protocol.
|
||||
* Use \Request::getRequestUri() instead.
|
||||
*/
|
||||
/*public static function currentUrl()
|
||||
{
|
||||
$url = \Request::urlFull();
|
||||
if (\Str::startsWith(config('app.url'), 'http://') && !\Str::startsWith($url, 'http://')) {
|
||||
$url = str_replace('https://', 'http://', $url);
|
||||
}
|
||||
if (\Str::startsWith(config('app.url'), 'https://') && !\Str::startsWith($url, 'https://')) {
|
||||
$url = str_replace('http://', 'https://', $url);
|
||||
}
|
||||
return $url;
|
||||
}*/
|
||||
|
||||
public static function isLocaleRtl(): bool
|
||||
{
|
||||
return in_array(app()->getLocale(), config("app.locales_rtl"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ class Mail
|
||||
const FETCH_SCHEDULE_EVERY_THIRTY_MINUTES = 30;
|
||||
const FETCH_SCHEDULE_HOURLY = 60;
|
||||
|
||||
const OAUTH_PROVIDER_MICROSOFT = 'ms';
|
||||
|
||||
/**
|
||||
* If reply is not extracted properly from the incoming email, add here a new separator.
|
||||
* Order is not important.
|
||||
@@ -57,6 +59,7 @@ class Mail
|
||||
|
||||
// Email service providers specific separators.
|
||||
'<div class="gmail_quote">', // Gmail
|
||||
'<div id="appendonsend"></div>', // Outlook / Live / Hotmail / Microsoft
|
||||
'<div name="quote" ',
|
||||
'yahoo_quoted_', // Yahoo, full: <div id=3D"ydp6h4f5c59yahoo_quoted_2937493705"
|
||||
'------------------ 原始邮件 ------------------', // QQ
|
||||
@@ -65,10 +68,10 @@ class Mail
|
||||
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0in 0in 0in">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
|
||||
|
||||
// General separators.
|
||||
'<blockquote', // General sepator
|
||||
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
|
||||
'<!-- originalMessage -->',
|
||||
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
|
||||
//'---Original---', // QQ separator, wait for emails from QQ and check
|
||||
'--------------- Original Message ---------------',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -80,13 +83,15 @@ class Mail
|
||||
* Configure mail sending parameters.
|
||||
*
|
||||
* @param App\Mailbox $mailbox
|
||||
* @param App\User $user_from
|
||||
* @param App\Conversation $conversation
|
||||
*/
|
||||
public static function setMailDriver($mailbox = null, $user_from = null)
|
||||
public static function setMailDriver($mailbox = null, $user_from = null, $conversation = null)
|
||||
{
|
||||
if ($mailbox) {
|
||||
// Configure mail driver according to Mailbox settings
|
||||
\Config::set('mail.driver', $mailbox->getMailDriverName());
|
||||
\Config::set('mail.from', $mailbox->getMailFrom($user_from));
|
||||
\Config::set('mail.from', $mailbox->getMailFrom($user_from, $conversation));
|
||||
|
||||
// SMTP
|
||||
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||
@@ -158,7 +163,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'));
|
||||
}
|
||||
@@ -188,6 +193,7 @@ class Mail
|
||||
$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();
|
||||
@@ -199,9 +205,11 @@ 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 ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,15 +304,7 @@ class Mail
|
||||
*/
|
||||
public static function fetchTest($mailbox)
|
||||
{
|
||||
$client = new Client([
|
||||
'host' => $mailbox->in_server,
|
||||
'port' => $mailbox->in_port,
|
||||
'encryption' => $mailbox->getInEncryptionName(),
|
||||
'validate_cert' => $mailbox->in_validate_cert,
|
||||
'username' => $mailbox->in_username,
|
||||
'password' => $mailbox->in_password,
|
||||
'protocol' => $mailbox->getInProtocolName(),
|
||||
]);
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
|
||||
// Connect to the Server
|
||||
$client->connect();
|
||||
@@ -318,7 +318,11 @@ class Mail
|
||||
// Get unseen messages for a period
|
||||
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
|
||||
|
||||
$last_error = $client->getLastError();
|
||||
$last_error = '';
|
||||
if (method_exists($client, 'getLastError')) {
|
||||
$last_error = $client->getLastError();
|
||||
}
|
||||
|
||||
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
|
||||
// Solution for MS mailboxes.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/176
|
||||
@@ -349,7 +353,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) {
|
||||
@@ -443,7 +447,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]);
|
||||
@@ -465,8 +469,10 @@ class Mail
|
||||
'x-autoreply' => '',
|
||||
'x-autorespond' => '',
|
||||
'auto-submitted' => 'auto-replied',
|
||||
'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) {
|
||||
@@ -480,6 +486,12 @@ class Mail
|
||||
if (strtolower($name) == $auto_header) {
|
||||
if (!$auto_header_value) {
|
||||
return true;
|
||||
} elseif (is_array($auto_header_value)) {
|
||||
foreach ($auto_header_value as $auto_header_value_item) {
|
||||
if ($value == $auto_header_value_item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} elseif ($value == $auto_header_value) {
|
||||
return true;
|
||||
}
|
||||
@@ -552,23 +564,76 @@ 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(),
|
||||
]);
|
||||
if (!$mailbox->oauthEnabled()) {
|
||||
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 {
|
||||
|
||||
\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',
|
||||
]);
|
||||
// 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 ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
'client_id' => $mailbox->in_username,
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'refresh_token' => $mailbox->oauthGetParam('r_token'),
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
|
||||
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
|
||||
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
|
||||
'error' => $error_message,
|
||||
'mailbox' => $mailbox->name,
|
||||
]);
|
||||
throw new \Exception($error_message, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// This makes it authenticate two times.
|
||||
//$cm->setTimeout(60);
|
||||
|
||||
return $cm->account('default');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate artificial Message-ID.
|
||||
*/
|
||||
public static function generateMessageId($email_address)
|
||||
public static function generateMessageId($email_address, $raw_body = '')
|
||||
{
|
||||
return 'fsdummy-'.str_random(16).'@'.preg_replace("/.*@/", '', $email_address);
|
||||
$hash = str_random(16);
|
||||
if ($raw_body) {
|
||||
$hash = md5(strval($raw_body));
|
||||
}
|
||||
|
||||
return 'fs-'.$hash.'@'.preg_replace("/.*@/", '', $email_address);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -586,10 +651,11 @@ class Mail
|
||||
$client = \MailHelper::getMailboxClient($mailbox);
|
||||
$client->connect();
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
|
||||
return null;
|
||||
}
|
||||
|
||||
$imap_folders = array_merge(['INBOX'], $mailbox->getInImapFolders());
|
||||
$imap_folders = $mailbox->getInImapFolders();
|
||||
|
||||
foreach ($imap_folders as $folder_name) {
|
||||
try {
|
||||
@@ -603,7 +669,10 @@ 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.
|
||||
@@ -617,10 +686,137 @@ 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_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$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 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;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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, [
|
||||
'connect_timeout' => 10,
|
||||
'form_params' => $params,
|
||||
]);
|
||||
} else {
|
||||
$params['v'] = config('app.version');
|
||||
return $client->request('GET', $url, [
|
||||
'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.
|
||||
*/
|
||||
|
||||
@@ -181,7 +181,7 @@ class Module extends Model
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
$list = explode(',', $required_extensions);
|
||||
$list = explode(',', $required_extensions ?? '');
|
||||
if (!is_array($list) || !count($list)) {
|
||||
return [];
|
||||
}
|
||||
@@ -228,4 +228,32 @@ class Module extends Model
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
public static function formatName($name)
|
||||
{
|
||||
return preg_replace("/ Module($|.*\[.*\]$)/", '$1', $name);
|
||||
}
|
||||
|
||||
public static function formatModuleData($module_data)
|
||||
{
|
||||
// Add (Third-Party).
|
||||
if (\App\Module::isOfficial($module_data['authorUrl'])
|
||||
&& $module_data['author'] != 'FreeScout'
|
||||
&& mb_substr(trim($module_data['name']), -1) != ']'
|
||||
) {
|
||||
$module_data['name'] = $module_data['name'].' ['.__('Third-Party').']';
|
||||
}
|
||||
return $module_data;
|
||||
}
|
||||
|
||||
public static function isThirdParty($module_data)
|
||||
{
|
||||
if (\App\Module::isOfficial($module_data['authorUrl'])
|
||||
&& $module_data['author'] != 'FreeScout'
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/EmailObserver.php
Normal file
18
app/Observers/EmailObserver.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Email;
|
||||
|
||||
class EmailObserver
|
||||
{
|
||||
/**
|
||||
* Email created.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function created(Email $email)
|
||||
{
|
||||
\Eventy::action('email.created', $email);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,11 @@ class ThreadObserver
|
||||
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;
|
||||
@@ -58,7 +62,11 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,26 @@ class ConversationPolicy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached version.
|
||||
*
|
||||
* @param User $user [description]
|
||||
* @param Conversation $conversation [description]
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function viewCached(User $user, Conversation $conversation)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
if ($conversation->mailbox->users_cached->contains($user)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the conversation.
|
||||
*
|
||||
|
||||
@@ -23,7 +23,7 @@ class FolderPolicy
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
if ($folder->user_id == $user->id || $user->mailboxes->contains($folder->mailbox)) {
|
||||
if ($folder->user_id == $user->id || $user->mailboxesSettings()->pluck('mailbox_id')->contains($folder->mailbox_id)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -38,7 +38,6 @@ class MailboxPolicy
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
// Use cached users for Realtime events
|
||||
if ($mailbox->users->contains($user)) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -68,6 +67,19 @@ class MailboxPolicy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can admin the mailbox.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function admin(User $user, Mailbox $mailbox)
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox.
|
||||
*
|
||||
@@ -78,7 +90,75 @@ class MailboxPolicy
|
||||
*/
|
||||
public function update(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
if ($user->isAdmin() || $user->canManageMailbox($mailbox->id)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox auto reply.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateAutoReply(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'auto')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox Permissions.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updatePermissions(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'perm')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox Permissions.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateSettings(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'edit')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mailbox Email Signature.
|
||||
*
|
||||
* @param \App\User $user
|
||||
* @param \App\Mailbox $mailbox
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function updateEmailSignature(User $user, Mailbox $mailbox)
|
||||
{
|
||||
if ($user->isAdmin() || $user->hasManageMailboxPermission($mailbox->id, 'sig')) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -20,9 +20,10 @@ class ThreadPolicy
|
||||
*/
|
||||
public function edit(User $user, Thread $thread)
|
||||
{
|
||||
if ($thread->created_by_user_id
|
||||
if (($thread->created_by_user_id
|
||||
&& in_array($thread->type, [Thread::TYPE_MESSAGE, Thread::TYPE_NOTE])
|
||||
&& ($user->isAdmin() || ($user->hasPermission(User::PERM_EDIT_CONVERSATIONS) && $thread->created_by_user_id == $user->id))
|
||||
&& ($user->isAdmin() || ($user->hasPermission(User::PERM_EDIT_CONVERSATIONS) && $thread->created_by_user_id == $user->id)))
|
||||
|| ($thread->created_by_customer_id && in_array($thread->type, [Thread::TYPE_CUSTOMER]))
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -35,7 +35,7 @@ class UserPolicy
|
||||
*/
|
||||
public function create(User $user)
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
if ($user->isAdmin() || $user->hasPermission(User::PERM_EDIT_USERS)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -52,7 +52,11 @@ class UserPolicy
|
||||
*/
|
||||
public function update(User $user, User $model)
|
||||
{
|
||||
if ($user->isAdmin() || $user->id == $model->id) {
|
||||
if ($user->isAdmin()
|
||||
|| $user->id == $model->id
|
||||
|| $user->hasPermission(User::PERM_EDIT_USERS)
|
||||
|| $user->canManageMailbox($model->id)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -101,10 +105,17 @@ class UserPolicy
|
||||
*/
|
||||
public function viewMailboxMenu(User $user)
|
||||
{
|
||||
if ($user->isAdmin() || \Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
|
||||
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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
\App\Mailbox::observe(\App\Observers\MailboxObserver::class);
|
||||
// Eloquent events for this table are not called automatically, so need to be called manually.
|
||||
//\App\MailboxUser::observe(\App\Observers\MailboxUserObserver::class);
|
||||
\App\Email::observe(\App\Observers\EmailObserver::class);
|
||||
\App\User::observe(\App\Observers\UserObserver::class);
|
||||
\App\Conversation::observe(\App\Observers\ConversationObserver::class);
|
||||
\App\Thread::observe(\App\Observers\ThreadObserver::class);
|
||||
@@ -53,6 +54,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
// Process module registration error - disable module and show error to admin
|
||||
\Eventy::addFilter('modules.register_error', function ($exception, $module) {
|
||||
|
||||
$msg = __('The :module_name module has been deactivated due to an error: :error_message', ['module_name' => $module->getName(), 'error_message' => $exception->getMessage()]);
|
||||
|
||||
\Log::error($msg);
|
||||
|
||||
// request() does is empty at this stage
|
||||
if (!empty($_POST['action']) && $_POST['action'] == 'activate') {
|
||||
|
||||
@@ -60,7 +65,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
\App\Module::deactiveModule($module->getAlias());
|
||||
|
||||
\Session::flash('flashes_floating', [[
|
||||
'text' => __('The :module_name module has been deactivated due to an error: :error_message', ['module_name' => $module->getName(), 'error_message' => $exception->getMessage()]),
|
||||
'text' => $msg,
|
||||
'type' => 'danger',
|
||||
'role' => \App\User::ROLE_ADMIN,
|
||||
]]);
|
||||
@@ -73,7 +78,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
\App\Module::deactiveModule($module->getAlias());
|
||||
|
||||
\Session::flash('flashes_floating', [[
|
||||
'text' => __('The :module_name module has been deactivated due to an error: :error_message', ['module_name' => $module->getName(), 'error_message' => $exception->getMessage()]),
|
||||
'text' => $msg,
|
||||
'type' => 'danger',
|
||||
'role' => \App\User::ROLE_ADMIN,
|
||||
]]);
|
||||
|
||||
@@ -13,6 +13,10 @@ class EventServiceProvider extends ServiceProvider
|
||||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
'Illuminate\Mail\Events\MessageSending' => [
|
||||
'App\Listeners\ProcessSwiftMessage',
|
||||
],
|
||||
|
||||
'Illuminate\Auth\Events\Registered' => [
|
||||
'App\Listeners\LogRegisteredUser',
|
||||
],
|
||||
@@ -64,6 +68,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\UserReplied' => [
|
||||
'App\Listeners\SendReplyToCustomer',
|
||||
'App\Listeners\SendNotificationToUsers',
|
||||
'App\Listeners\RefreshConversations',
|
||||
],
|
||||
|
||||
'App\Events\CustomerReplied' => [
|
||||
@@ -73,6 +78,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\UserCreatedConversation' => [
|
||||
'App\Listeners\SendReplyToCustomer',
|
||||
'App\Listeners\SendNotificationToUsers',
|
||||
'App\Listeners\RefreshConversations',
|
||||
],
|
||||
|
||||
'App\Events\CustomerCreatedConversation' => [
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Notifications\BroadcastNotification;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Broadcasting\BroadcastManager;
|
||||
@@ -78,8 +79,8 @@ class PolycastServiceProvider extends ServiceProvider
|
||||
$payload = $collection->map(function ($item, $key) use ($request) {
|
||||
$created = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $item->created_at);
|
||||
$requested = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $request->get('time'));
|
||||
$item->channels = json_decode($item->channels);
|
||||
$item->payload = json_decode($item->payload);
|
||||
$item->channels = json_decode($item->channels, false);
|
||||
$item->payload = json_decode($item->payload, false);
|
||||
// Add extra data to the payload
|
||||
// This works only if payload has medius and thread_id
|
||||
$item->data = BroadcastNotification::fetchPayloadData($item->payload);
|
||||
@@ -118,6 +119,13 @@ class PolycastServiceProvider extends ServiceProvider
|
||||
$viewing_conversation_id = null;
|
||||
if (!empty($request->data) && !empty($request->data['conversation_id'])) {
|
||||
$viewing_conversation_id = $request->data['conversation_id'];
|
||||
|
||||
if (!empty($request->data['conversation_view_focus'])) {
|
||||
$conversation = Conversation::find($request->data['conversation_id']);
|
||||
if ($conversation) {
|
||||
\Eventy::action('conversation.view.focus', $conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($viewing_conversation_id) {
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function map()
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
//$this->mapApiRoutes();
|
||||
|
||||
$this->mapWebRoutes();
|
||||
|
||||
@@ -71,11 +71,11 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mapApiRoutes()
|
||||
{
|
||||
Route::prefix('api')
|
||||
->middleware('api')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/api.php'));
|
||||
}
|
||||
// protected function mapApiRoutes()
|
||||
// {
|
||||
// Route::prefix('api')
|
||||
// ->middleware('api')
|
||||
// ->namespace($this->namespace)
|
||||
// ->group(base_path('routes/api.php'));
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ 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;
|
||||
const EVENT_CUSTOMER_REPLIED_TO_ASSIGNED = 7;
|
||||
// Notify me when another Help Scout user replies or adds a note…
|
||||
// Notify me when another a user replies or adds a note…
|
||||
const EVENT_USER_REPLIED_TO_UNASSIGNED = 8;
|
||||
const EVENT_USER_REPLIED_TO_MY = 5;
|
||||
const EVENT_USER_REPLIED_TO_ASSIGNED = 9;
|
||||
@@ -41,6 +41,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,16 +53,14 @@ 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,
|
||||
],
|
||||
@@ -96,7 +95,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,16 +158,16 @@ class Subscription extends Model
|
||||
*/
|
||||
public static function usersToNotify($event_type, $conversation, $threads, $mailbox_user_ids = null)
|
||||
{
|
||||
// We can not check imported here, as after conversation has been imported via API
|
||||
// notifications has to be sent.
|
||||
// if ($conversation->imported) {
|
||||
// return true;
|
||||
// }
|
||||
$thread = $threads[0];
|
||||
|
||||
// Ignore imported threads.
|
||||
if ($thread->imported) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Detect events
|
||||
$events = [];
|
||||
|
||||
$thread = $threads[0];
|
||||
$prev_thread = null;
|
||||
if (!empty($threads[1])) {
|
||||
$prev_thread = $threads[1];
|
||||
@@ -199,8 +203,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 +239,14 @@ 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;
|
||||
}
|
||||
@@ -249,10 +258,32 @@ class Subscription extends Model
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if user muted notifications for this mailbox
|
||||
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_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 (\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;
|
||||
}
|
||||
|
||||
@@ -295,7 +326,7 @@ class Subscription extends Model
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -320,17 +351,20 @@ 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 (!empty($notify[self::MEDIUM_EMAIL]) || !empty($notify[self::MEDIUM_MENU])) {
|
||||
|
||||
$notify_menu = ($notify[self::MEDIUM_EMAIL] ?? []) + ($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);
|
||||
|
||||
346
app/Thread.php
346
app/Thread.php
@@ -2,6 +2,15 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Events\ConversationStatusChanged;
|
||||
use App\Events\ConversationUserChanged;
|
||||
use App\Events\UserAddedNote;
|
||||
use App\Events\CustomerCreatedConversation;
|
||||
use App\Events\UserCreatedConversation;
|
||||
use App\Events\UserCreatedConversationDraft;
|
||||
use App\Events\UserCreatedThreadDraft;
|
||||
use App\Events\UserReplied;
|
||||
use App\Events\CustomerReplied;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Thread extends Model
|
||||
@@ -144,6 +153,12 @@ class Thread extends Model
|
||||
self::SOURCE_TYPE_API => 'api',
|
||||
];
|
||||
|
||||
// Metas.
|
||||
const META_CONVERSATION_HISTORY = 'ch';
|
||||
const META_PREV_CONVERSATION = 'pc';
|
||||
const META_MERGED_WITH_CONV = 'mwc';
|
||||
const META_MERGED_INTO_CONV = 'mic';
|
||||
|
||||
protected $dates = [
|
||||
'opened_at',
|
||||
'created_at',
|
||||
@@ -278,9 +293,9 @@ class Thread extends Model
|
||||
/**
|
||||
* Convert body to plain text.
|
||||
*/
|
||||
public function getBodyAsText()
|
||||
public function getBodyAsText($options = array())
|
||||
{
|
||||
return \Helper::htmlToText($this->body);
|
||||
return \Helper::htmlToText($this->body, true, $options);
|
||||
}
|
||||
|
||||
public function getBodyWithFormatedLinks(string $body = '') :string
|
||||
@@ -529,7 +544,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) {
|
||||
@@ -553,9 +570,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 = '';
|
||||
|
||||
@@ -574,7 +592,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]);
|
||||
}
|
||||
@@ -594,6 +612,21 @@ class Thread extends Model
|
||||
$did_this = __(":person restored");
|
||||
} elseif ($this->action_type == self::ACTION_TYPE_MOVED_FROM_MAILBOX) {
|
||||
$did_this = __(":person moved conversation from another mailbox");
|
||||
} elseif ($this->action_type == self::ACTION_TYPE_MERGED) {
|
||||
if (!empty($this->getMeta(Thread::META_MERGED_WITH_CONV))) {
|
||||
$did_this = __(":person merged with another conversation");
|
||||
} else {
|
||||
$merge_conversation = Conversation::find($this->getMeta(Thread::META_MERGED_INTO_CONV));
|
||||
$merge_conversation_number = '';
|
||||
if ($merge_conversation) {
|
||||
$merge_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) {
|
||||
if (empty($this->edited_by_user_id)) {
|
||||
@@ -613,7 +646,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);
|
||||
@@ -633,11 +666,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);
|
||||
@@ -859,6 +892,280 @@ class Thread extends Model
|
||||
return $thread;
|
||||
}
|
||||
|
||||
public static function createExtended($data = [], $conversation = null, $customer = null, $update_conv = true)
|
||||
{
|
||||
if (empty($data['type']) || empty($data['body'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_customer = ($data['type'] == Thread::TYPE_CUSTOMER);
|
||||
|
||||
if (!$customer && !empty($data['customer_id'])) {
|
||||
$customer = Customer::find($data['customer_id']);
|
||||
}
|
||||
if (!$customer) {
|
||||
$customer = $conversation->customer;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
return false;
|
||||
//return $this->getErrorResponse('`customer` parameter is required', 'customer');
|
||||
}
|
||||
if (($data['type'] == Thread::TYPE_MESSAGE || $data['type'] == Thread::TYPE_NOTE) && empty($user_id)) {
|
||||
return false;
|
||||
//return $this->getErrorResponse('`user` parameter is required', 'user');
|
||||
}
|
||||
|
||||
// Create thread.
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// New conversation.
|
||||
$new = !$conversation->threads_count;
|
||||
|
||||
$thread = new Thread();
|
||||
$thread->conversation_id = $conversation->id;
|
||||
$thread->type = $data['type'];
|
||||
if ($is_customer) {
|
||||
$thread->source_via = Thread::PERSON_CUSTOMER;
|
||||
$thread->created_by_customer_id = $customer->id;
|
||||
} else {
|
||||
$thread->source_via = Thread::PERSON_USER;
|
||||
$thread->created_by_user_id = $user_id;
|
||||
$thread->edited_by_user_id = null;
|
||||
$thread->edited_at = null;
|
||||
}
|
||||
$thread->source_type = Thread::SOURCE_TYPE_API;
|
||||
$thread->state = $data['state'] ?? Thread::STATE_PUBLISHED;
|
||||
$thread->customer_id = $customer->id ?? $conversation->customer_id ?? null;
|
||||
$thread->body = $data['body'];
|
||||
if (!$is_customer) {
|
||||
$thread->setTo([$customer->getMainEmail()]);
|
||||
}
|
||||
|
||||
$cc = \MailHelper::sanitizeEmails($data['cc'] ?? []);
|
||||
$thread->setCc($cc);
|
||||
|
||||
$bcc = \MailHelper::sanitizeEmails($data['bcc'] ?? []);
|
||||
$thread->setBcc($bcc);
|
||||
$thread->imported = (int)($data['imported'] ?? false);
|
||||
if ($thread->imported && !empty($data['created_at'])) {
|
||||
$thread->created_at = self::utcStringToServerDate($data['created_at']);
|
||||
}
|
||||
if ($new) {
|
||||
$thread->first = true;
|
||||
}
|
||||
|
||||
// Assignee.
|
||||
if (empty($data['user_id'])) {
|
||||
$thread->user_id = $conversation->user_id;
|
||||
}
|
||||
|
||||
// Process attachments.
|
||||
if (!empty($data['attachments'])) {
|
||||
$has_attachments = false;
|
||||
foreach ($data['attachments'] as $attachment) {
|
||||
|
||||
$content = null;
|
||||
$uploaded_file = null;
|
||||
|
||||
if (is_object($attachment) && get_class($attachment) == 'Illuminate\Http\UploadedFile') {
|
||||
|
||||
$uploaded_file = $attachment;
|
||||
$attachment = [];
|
||||
$attachment['file_name'] = $uploaded_file->getClientOriginalName();
|
||||
$attachment['mime_type'] = $uploaded_file->getMimeType();
|
||||
|
||||
} else {
|
||||
|
||||
if (empty($attachment['file_name'])
|
||||
//|| empty($attachment['mime_type'])
|
||||
|| (empty($attachment['data']) && empty($attachment['file_url']))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($attachment['data'])) {
|
||||
// BASE64 string.
|
||||
$content = base64_decode($attachment['data']);
|
||||
if (!$content) {
|
||||
continue;
|
||||
}
|
||||
if (empty($attachment['mime_type'])) {
|
||||
$f = finfo_open();
|
||||
$attachment['mime_type'] = finfo_buffer($f, $content, FILEINFO_MIME_TYPE);
|
||||
}
|
||||
} else {
|
||||
// URL.
|
||||
$uploaded_file = \Helper::downloadRemoteFileAsTmpFile($attachment['file_url']);
|
||||
if (!$uploaded_file) {
|
||||
continue;
|
||||
}
|
||||
if (empty($attachment['mime_type'])) {
|
||||
$attachment['mime_type'] = $uploaded_file->getClientMimeType();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$has_attachments) {
|
||||
$thread->save();
|
||||
}
|
||||
$attachment = Attachment::create(
|
||||
$attachment['file_name'],
|
||||
$attachment['mime_type'],
|
||||
null,
|
||||
$content,
|
||||
$uploaded_file = $uploaded_file,
|
||||
$embedded = false,
|
||||
$thread->id,
|
||||
$user_id ?? null
|
||||
);
|
||||
|
||||
if ($attachment) {
|
||||
$has_attachments = true;
|
||||
}
|
||||
}
|
||||
if ($has_attachments) {
|
||||
$thread->has_attachments = true;
|
||||
$conversation->has_attachments = true;
|
||||
}
|
||||
}
|
||||
|
||||
$thread->save();
|
||||
|
||||
if ($new) {
|
||||
if ($is_customer) {
|
||||
$conversation->source_via = Conversation::PERSON_CUSTOMER;
|
||||
$conversation->created_by_customer_id = $customer->id;
|
||||
} else {
|
||||
$conversation->source_via = Conversation::PERSON_USER;
|
||||
$conversation->created_by_user_id = $user_id;
|
||||
}
|
||||
}
|
||||
|
||||
$conversation->setCc($cc);
|
||||
// BCC should keep BCC of the first email,
|
||||
// so we change BCC only if it contains emails.
|
||||
if ($bcc) {
|
||||
$conversation->setBcc($bcc);
|
||||
}
|
||||
|
||||
$update_folder = false;
|
||||
|
||||
if ($thread->isReply()) {
|
||||
$conversation->last_reply_at = $now;
|
||||
if ($is_customer) {
|
||||
$conversation->last_reply_from = Conversation::PERSON_CUSTOMER;
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
if ($is_customer) {
|
||||
if ($new) {
|
||||
$conversation = \Eventy::filter('conversation.created_by_customer', $conversation, $thread, $customer);
|
||||
} else {
|
||||
$conversation = \Eventy::filter('conversation.customer_replied', $conversation, $thread, $customer);
|
||||
}
|
||||
}
|
||||
// save() will check if something in the model has changed. If it hasn't it won't run a db query.
|
||||
$conversation->save();
|
||||
|
||||
// Update folders counters
|
||||
if (!$new) {
|
||||
// Update folders counters
|
||||
$conversation->mailbox->updateFoldersCounters();
|
||||
}
|
||||
|
||||
// Events.
|
||||
|
||||
// Conversation customer changed
|
||||
// Not used anywhere
|
||||
// if ($prev_customer_id) {
|
||||
// event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
|
||||
// }
|
||||
|
||||
if ($new) {
|
||||
if ($is_customer) {
|
||||
event(new CustomerCreatedConversation($conversation, $thread));
|
||||
\Eventy::action('conversation.created_by_customer', $conversation, $thread, $customer);
|
||||
} else {
|
||||
// New conversation.
|
||||
event(new UserCreatedConversation($conversation, $thread));
|
||||
\Eventy::action('conversation.created_by_user_can_undo', $conversation, $thread);
|
||||
// After Conversation::UNDO_TIMOUT period trigger final event.
|
||||
\Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
}
|
||||
} elseif ($data['type'] == Thread::TYPE_NOTE) {
|
||||
// Note.
|
||||
event(new UserAddedNote($conversation, $thread));
|
||||
\Eventy::action('conversation.note_added', $conversation, $thread);
|
||||
} else {
|
||||
// Reply.
|
||||
if ($is_customer) {
|
||||
event(new CustomerReplied($conversation, $thread));
|
||||
\Eventy::action('conversation.customer_replied', $conversation, $thread, $customer);
|
||||
} else {
|
||||
event(new UserReplied($conversation, $thread));
|
||||
\Eventy::action('conversation.user_replied_can_undo', $conversation, $thread);
|
||||
// After Conversation::UNDO_TIMOUT period trigger final event.
|
||||
\Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
|
||||
}
|
||||
}
|
||||
|
||||
return $thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name of the user who edited thread.
|
||||
*/
|
||||
@@ -920,6 +1227,18 @@ class Thread extends Model
|
||||
$this->setMetas($metas);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -1003,4 +1322,15 @@ class Thread extends Model
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function getActionTypeName()
|
||||
{
|
||||
if (!$this->action_type) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$action_types = \Eventy::filter('thread.action_types', self::$action_types);
|
||||
|
||||
return self::$action_types[$this->action_type] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
354
app/User.php
354
app/User.php
@@ -6,6 +6,7 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Email;
|
||||
use App\Mail\PasswordChanged;
|
||||
use App\Mail\UserInvite;
|
||||
use App\Notifications\WebsiteNotification;
|
||||
@@ -31,6 +32,8 @@ class User extends Authenticatable
|
||||
|
||||
const EMAIL_MAX_LENGTH = 100;
|
||||
|
||||
const EMAIL_DELETED_SUFFIX = '_deleted';
|
||||
|
||||
/**
|
||||
* Roles.
|
||||
*/
|
||||
@@ -52,7 +55,7 @@ class User extends Authenticatable
|
||||
* Statuses.
|
||||
*/
|
||||
const STATUS_ACTIVE = 1;
|
||||
const STATUS_DISABLED = 2; // todo
|
||||
const STATUS_DISABLED = 2;
|
||||
const STATUS_DELETED = 3;
|
||||
|
||||
/**
|
||||
@@ -72,15 +75,19 @@ class User extends Authenticatable
|
||||
* Global user permissions.
|
||||
*/
|
||||
const PERM_DELETE_CONVERSATIONS = 1;
|
||||
const PERM_EDIT_CONVERSATIONS = 2;
|
||||
const PERM_EDIT_SAVED_REPLIES = 3;
|
||||
const PERM_EDIT_TAGS = 4;
|
||||
const PERM_EDIT_CONVERSATIONS = 2;
|
||||
const PERM_EDIT_SAVED_REPLIES = 3;
|
||||
const PERM_EDIT_TAGS = 4;
|
||||
const PERM_EDIT_CUSTOM_FOLDERS = 5;
|
||||
const PERM_EDIT_USERS = 10;
|
||||
|
||||
public static $user_permissions = [
|
||||
self::PERM_DELETE_CONVERSATIONS,
|
||||
self::PERM_EDIT_CONVERSATIONS,
|
||||
self::PERM_EDIT_SAVED_REPLIES,
|
||||
self::PERM_EDIT_TAGS,
|
||||
self::PERM_EDIT_CUSTOM_FOLDERS,
|
||||
self::PERM_EDIT_USERS,
|
||||
];
|
||||
|
||||
const WEBSITE_NOTIFICATIONS_PAGE_SIZE = 25;
|
||||
@@ -107,8 +114,12 @@ 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',
|
||||
];
|
||||
|
||||
/**
|
||||
* For array_unique function.
|
||||
*
|
||||
@@ -124,7 +135,7 @@ class User extends Authenticatable
|
||||
*/
|
||||
public function mailboxes()
|
||||
{
|
||||
return $this->belongsToMany('App\Mailbox')->as('settings')->withPivot('after_send');
|
||||
return $this->belongsToMany('App\Mailbox');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +146,15 @@ class User extends Authenticatable
|
||||
return $this->mailboxes()->rememberForever();
|
||||
}
|
||||
|
||||
public function mailboxesWithSettings()
|
||||
{
|
||||
return $this->belongsToMany('App\Mailbox')->as('settings')
|
||||
->withPivot('after_send')
|
||||
->withPivot('hide')
|
||||
->withPivot('mute')
|
||||
->withPivot('access');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversations assigned to user.
|
||||
*/
|
||||
@@ -191,7 +211,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,19 +231,71 @@ class User extends Authenticatable
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
if ($cache) {
|
||||
return Mailbox::rememberForever()->get();
|
||||
$mailboxes = Mailbox::rememberForever()->get();
|
||||
} else {
|
||||
return Mailbox::all();
|
||||
$mailboxes = Mailbox::all();
|
||||
}
|
||||
} else {
|
||||
if ($cache) {
|
||||
return $this->mailboxes_cached;
|
||||
$mailboxes = $this->mailboxes_cached;
|
||||
} else {
|
||||
return $this->mailboxes;
|
||||
$mailboxes = $this->mailboxes;
|
||||
}
|
||||
}
|
||||
|
||||
return $mailboxes->sortBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mailboxes to which user has access.
|
||||
*/
|
||||
public function mailboxesCanViewWithSettings($cache = false)
|
||||
{
|
||||
$user = $this;
|
||||
|
||||
if ($this->isAdmin()) {
|
||||
$query = Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
|
||||
->leftJoin('mailbox_user', function ($join) use ($user) {
|
||||
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
|
||||
$join->where('mailbox_user.user_id', $user->id);
|
||||
});
|
||||
} else {
|
||||
$query = Mailbox::select(['mailboxes.*', 'mailbox_user.hide', 'mailbox_user.mute', 'mailbox_user.access'])
|
||||
->join('mailbox_user', function ($join) use ($user) {
|
||||
$join->on('mailbox_user.mailbox_id', '=', 'mailboxes.id');
|
||||
$join->where('mailbox_user.user_id', $user->id);
|
||||
});
|
||||
}
|
||||
if ($cache) {
|
||||
return $query->rememberForever()->get();
|
||||
} else {
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
|
||||
public function mailboxesSettings($cache = true)
|
||||
{
|
||||
$user = $this;
|
||||
|
||||
$query = MailboxUser::where('user_id', $user->id);
|
||||
|
||||
if ($cache) {
|
||||
return $query->rememberForever()->get();
|
||||
} else {
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
|
||||
public function mailboxSettings($mailbox_id)
|
||||
{
|
||||
$settings = $this->mailboxesSettings()->where('mailbox_id', $mailbox_id)->first();
|
||||
|
||||
if (!$settings) {
|
||||
return Mailbox::getDummySettings();
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
/**
|
||||
* Get IDs of mailboxes to which user has access.
|
||||
*/
|
||||
@@ -242,6 +314,53 @@ class User extends Authenticatable
|
||||
return in_array($mailbox_id, $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the user can manage any mailboxes
|
||||
*/
|
||||
public function hasManageMailboxAccess() {
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
//$mailboxes = $this->mailboxesCanViewWithSettings(true);
|
||||
$mailboxes = $this->mailboxesSettings();
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
if (!empty($mailbox->access) && !empty(json_decode($mailbox->access))) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the user can manage a specific mailbox
|
||||
*/
|
||||
public function canManageMailbox($mailbox_id)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
} 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 ?? ''))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function hasManageMailboxPermission($mailbox_id, $perm) {
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
} else {
|
||||
//$mailbox = $this->mailboxesCanViewWithSettings(true)->where('id', $mailbox_id)->first();
|
||||
$mailbox = $this->mailboxesSettings()->where('mailbox_id', $mailbox_id)->first();
|
||||
if ($mailbox && !empty($mailbox->access) && in_array($perm, json_decode($mailbox->access))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generate random password for the user.
|
||||
*
|
||||
@@ -305,11 +424,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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,38 +441,53 @@ class User extends Authenticatable
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function dateFormat($date, $format = 'M j, Y H:i', $user = null)
|
||||
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();
|
||||
}
|
||||
if (is_string($date)) {
|
||||
// Convert string in to Carbon
|
||||
$date = Carbon::parse($date);
|
||||
try {
|
||||
$date = Carbon::parse($date);
|
||||
} catch (\Exception $e) {
|
||||
$date = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($user) {
|
||||
if ($user->time_format == self::TIME_FORMAT_12) {
|
||||
$format = strtr($format, [
|
||||
'H' => 'h',
|
||||
'G' => 'g',
|
||||
':i' => ':ia',
|
||||
':ia:s' => ':i:sa',
|
||||
]);
|
||||
} else {
|
||||
$format = strtr($format, [
|
||||
'h' => 'H',
|
||||
'g' => 'G',
|
||||
':ia' => ':i',
|
||||
':i:sa' => ':i:s',
|
||||
]);
|
||||
if (!$date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$format) {
|
||||
$format = 'M j, Y H:i';
|
||||
}
|
||||
|
||||
if ($user && $user !== false) {
|
||||
if ($modify_format) {
|
||||
if ($user->time_format == self::TIME_FORMAT_12) {
|
||||
$format = strtr($format, [
|
||||
'H' => 'h',
|
||||
'G' => 'g',
|
||||
':i' => ':ia',
|
||||
':ia:s' => ':i:sa',
|
||||
]);
|
||||
} else {
|
||||
$format = strtr($format, [
|
||||
'h' => 'H',
|
||||
'g' => 'G',
|
||||
':ia' => ':i',
|
||||
':i:sa' => ':i:s',
|
||||
]);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
} else {
|
||||
return $date->format($format);
|
||||
if ($use_user_timezone) {
|
||||
$date->setTimezone($user->timezone);
|
||||
}
|
||||
}
|
||||
return $date->format($format);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,7 +524,7 @@ class User extends Authenticatable
|
||||
}
|
||||
} else {
|
||||
$diff_text = $date->diffForHumans();
|
||||
$diff_text = preg_replace('/minutes?/', 'min', $diff_text);
|
||||
$diff_text = preg_replace('/minute[sn]?/', 'min', $diff_text);
|
||||
|
||||
return $diff_text;
|
||||
}
|
||||
@@ -421,16 +555,18 @@ 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'),
|
||||
self::PERM_EDIT_CUSTOM_FOLDERS => __('Users are allowed to manage custom folders'),
|
||||
self::PERM_EDIT_USERS => __('Users are allowed to manage users'),
|
||||
];
|
||||
|
||||
if (!empty($user_permission_names[$user_permission])) {
|
||||
return $user_permission_names[$user_permission];
|
||||
} else {
|
||||
return \Event::fire('filter.user_permission_name', [$user_permission]);
|
||||
return \Eventy::filter('user_permissions.name', '', $user_permission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,10 +777,17 @@ class User extends Authenticatable
|
||||
|
||||
/**
|
||||
* Resize and save user photo.
|
||||
*
|
||||
* $uploaded_file can be \File or string.
|
||||
*/
|
||||
public function savePhoto($uploaded_file)
|
||||
public function savePhoto($uploaded_file, $mime_type = '')
|
||||
{
|
||||
$resized_image = \App\Misc\Helper::resizeImage($uploaded_file->getRealPath(), $uploaded_file->getMimeType(), self::PHOTO_SIZE, self::PHOTO_SIZE);
|
||||
$real_path = $uploaded_file;
|
||||
if (!is_string($uploaded_file)) {
|
||||
$real_path = $uploaded_file->getRealPath();
|
||||
$mime_type = $uploaded_file->getMimeType();
|
||||
}
|
||||
$resized_image = \App\Misc\Helper::resizeImage($real_path, $mime_type, self::PHOTO_SIZE, self::PHOTO_SIZE);
|
||||
|
||||
if (!$resized_image) {
|
||||
return false;
|
||||
@@ -682,14 +825,44 @@ class User extends Authenticatable
|
||||
$this->photo_url = '';
|
||||
}
|
||||
|
||||
public function hasPermission($permission)
|
||||
public function hasPermission($permission, $check_own_permissions = true)
|
||||
{
|
||||
$permissions = Option::get('user_permissions');
|
||||
if (!empty($permissions) && is_array($permissions) && in_array($permission, $permissions)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
$has_permission = false;
|
||||
|
||||
$global_permissions = self::getGlobalUserPermissions();
|
||||
|
||||
if (!empty($global_permissions) && is_array($global_permissions) && in_array($permission, $global_permissions)) {
|
||||
$has_permission = true;
|
||||
}
|
||||
|
||||
if ($check_own_permissions && !empty($this->permissions)) {
|
||||
if (isset($this->permissions[$permission])) {
|
||||
$has_permission = (bool)$this->permissions[$permission];
|
||||
}
|
||||
}
|
||||
|
||||
return $has_permission;
|
||||
}
|
||||
|
||||
public static function getGlobalUserPermissions()
|
||||
{
|
||||
$permissions = [];
|
||||
$permissions_json = config('app.user_permissions');
|
||||
|
||||
if ($permissions_json) {
|
||||
$permissions_json = base64_decode($permissions_json);
|
||||
try {
|
||||
$permissions = json_decode($permissions_json, true);
|
||||
} catch (\Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($permissions)) {
|
||||
$permissions = [];
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -714,10 +887,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();
|
||||
@@ -728,6 +898,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'])) {
|
||||
$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.
|
||||
*/
|
||||
@@ -778,6 +978,16 @@ class User extends Authenticatable
|
||||
return self::where('status', '!=', self::STATUS_DELETED);
|
||||
}
|
||||
|
||||
public function isActive()
|
||||
{
|
||||
return $this->status == self::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
public function isDisabled()
|
||||
{
|
||||
return $this->status == self::STATUS_DISABLED;
|
||||
}
|
||||
|
||||
public function isDeleted()
|
||||
{
|
||||
return $this->status == self::STATUS_DELETED;
|
||||
@@ -804,6 +1014,7 @@ class User extends Authenticatable
|
||||
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||
})
|
||||
->whereIn('mailbox_user.mailbox_id', $mailbox_ids)
|
||||
->groupBy('users.id')
|
||||
->get();
|
||||
}
|
||||
|
||||
@@ -848,4 +1059,45 @@ class User extends Authenticatable
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
public static function getUserPermissionsList()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
177
composer.json
177
composer.json
@@ -10,14 +10,14 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.0.0",
|
||||
"php": ">=7.1.0",
|
||||
|
||||
"symfony/debug": "v3.4.18",
|
||||
"devfactory/minify": "1.0.7",
|
||||
"fideloper/proxy": "3.3.4",
|
||||
"laravel/framework": "v5.5.40",
|
||||
"laravel/tinker": "v1.0.7",
|
||||
"mews/purifier": "2.1.0",
|
||||
"mews/purifier": "3.2.2",
|
||||
"spatie/laravel-activitylog": "2.7.0",
|
||||
"lord/laroute": "2.4.7",
|
||||
"axn/laravel-laroute": "1.3.0",
|
||||
@@ -29,22 +29,26 @@
|
||||
"barryvdh/laravel-translation-manager": "v0.5.0",
|
||||
"chumper/zipper": "v1.0.2",
|
||||
"rachidlaasri/laravel-installer": "4.0.2",
|
||||
"kitetail/zttp": "v0.4.0",
|
||||
"rap2hpoutre/laravel-log-viewer": "v0.22.1",
|
||||
"rap2hpoutre/laravel-log-viewer": "v2.0.0",
|
||||
"codedge/laravel-selfupdater": "1.4.3",
|
||||
"doctrine/dbal": "v2.5.13"
|
||||
"doctrine/dbal": "2.12.1",
|
||||
"egulias/email-validator": "2.1.10",
|
||||
"symfony/console": "v3.4.47",
|
||||
"javoscript/laravel-macroable-models": "1.0.4",
|
||||
"ramsey/uuid": "3.9.6",
|
||||
"symfony/polyfill-mbstring": "v1.24.0",
|
||||
"webklex/php-imap": "dev-master#f565c3b2510238191981dec92840bfcf3cc8b7e8"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "v3.2.0",
|
||||
"filp/whoops": "2.2.0",
|
||||
"fzaninotto/faker": "v1.7.1",
|
||||
"filp/whoops": "2.14.5",
|
||||
"fzaninotto/faker": "v1.9.2",
|
||||
"mockery/mockery": "1.1.0",
|
||||
"phpunit/phpunit": "6.5.8",
|
||||
|
||||
"symfony/polyfill-ctype": "v1.10.*",
|
||||
"vlucas/phpdotenv": "v2.5.1",
|
||||
"symfony/css-selector": "v3.4.18",
|
||||
"symfony/polyfill-mbstring": "v1.10.*",
|
||||
"symfony/var-dumper": "v3.4.18",
|
||||
"symfony/routing": "v3.4.18",
|
||||
"symfony/process": "v3.4.18",
|
||||
@@ -54,9 +58,7 @@
|
||||
"psr/log": "1.0.*",
|
||||
"symfony/http-kernel": "v3.4.18",
|
||||
"symfony/finder": "v3.4.18",
|
||||
"symfony/console": "v3.4.18",
|
||||
"doctrine/lexer": "v1.0.1",
|
||||
"egulias/email-validator": "2.1.6",
|
||||
"swiftmailer/swiftmailer": "v6.1.*",
|
||||
"symfony/translation": "v3.4.18",
|
||||
"nesbot/carbon": "1.35.*",
|
||||
@@ -64,14 +66,15 @@
|
||||
"erusev/parsedown": "1.7.1",
|
||||
"doctrine/inflector": "v1.2.*",
|
||||
"guzzlehttp/psr7": "1.4.*",
|
||||
"tedivm/jshrink": "v1.3.1",
|
||||
"tedivm/jshrink": "1.4.0",
|
||||
"nikic/php-parser": "v4.1.0",
|
||||
"doctrine/annotations": "v1.4.*",
|
||||
"doctrine/cache": "v1.6.*",
|
||||
"doctrine/collections": "v1.4.*",
|
||||
"doctrine/instantiator": "1.0.5",
|
||||
"myclabs/deep-copy": "1.7.0",
|
||||
"webmozart/assert": "1.3.0"
|
||||
"webmozart/assert": "1.3.0",
|
||||
"psr/container": "1.0.0"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
@@ -79,32 +82,71 @@
|
||||
"database/factories"
|
||||
],
|
||||
"psr-0": {
|
||||
"Swift_": "overrides/Swiftmailer/lib/classes"
|
||||
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes",
|
||||
"HTMLPurifier_": "overrides/ezyang/htmlpurifier/library"
|
||||
},
|
||||
"fs-comment": "Above new lines are listed files containing custom updates. Below - only PHP compatibility updates.",
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Modules\\": "Modules/",
|
||||
"Axn\\Laroute\\Routes\\": "overrides/Axn/Laroute/Routes/",
|
||||
"Webklex\\IMAP\\": "overrides/Webklex/IMAP/",
|
||||
"RachidLaasri\\LaravelInstaller\\Helpers\\": "overrides/RachidLaasri/LaravelInstaller/Helpers/",
|
||||
"RachidLaasri\\LaravelInstaller\\Middleware\\": "overrides/RachidLaasri/LaravelInstaller/Middleware/",
|
||||
"RachidLaasri\\LaravelInstaller\\Controllers\\": "overrides/RachidLaasri/LaravelInstaller/Controllers/",
|
||||
"RachidLaasri\\LaravelInstaller\\Providers\\": "overrides/RachidLaasri/LaravelInstaller/Providers/",
|
||||
"Nwidart\\Modules\\": "overrides/Nwidart/Modules/",
|
||||
"Codedge\\Updater\\SourceRepositoryTypes\\": "overrides/Codedge/Updater/SourceRepositoryTypes/",
|
||||
"Barryvdh\\TranslationManager\\": "overrides/Barryvdh/TranslationManager/",
|
||||
"Illuminate\\Foundation\\": "overrides/Illuminate/Foundation/",
|
||||
"Illuminate\\Routing\\": "overrides/Illuminate/Routing/",
|
||||
"Illuminate\\Validation\\Concerns\\": "overrides/Illuminate/Validation/Concerns/",
|
||||
"Illuminate\\Mail\\": "overrides/Illuminate/Mail/",
|
||||
"Lord\\Laroute\\Routes\\": "overrides/Lord/Laroute/Routes/",
|
||||
"TorMorten\\Eventy\\": "overrides/TorMorten/Eventy/",
|
||||
"Symfony\\Component\\Debug\\": "overrides/Symfony/Component/Debug/"
|
||||
"Axn\\Laroute\\Routes\\": "overrides/axn/laravel-laroute/src/Routes/",
|
||||
"Webklex\\IMAP\\": "overrides/webklex/laravel-imap/src/IMAP/",
|
||||
"RachidLaasri\\LaravelInstaller\\Helpers\\": "overrides/rachidlaasri/laravel-installer/src/Helpers/",
|
||||
"RachidLaasri\\LaravelInstaller\\Middleware\\": "overrides/rachidlaasri/laravel-installer/src/Middleware/",
|
||||
"RachidLaasri\\LaravelInstaller\\Controllers\\": "overrides/rachidlaasri/laravel-installer/src/Controllers/",
|
||||
"RachidLaasri\\LaravelInstaller\\Providers\\": "overrides/rachidlaasri/laravel-installer/src/Providers/",
|
||||
"Nwidart\\Modules\\": "overrides/nwidart/laravel-modules/src/",
|
||||
"Codedge\\Updater\\SourceRepositoryTypes\\": "overrides/codedge/laravel-selfupdater/src/SourceRepositoryTypes/",
|
||||
"Barryvdh\\TranslationManager\\": "overrides/barryvdh/laravel-translation-manager/src/",
|
||||
"Illuminate\\Foundation\\": "overrides/laravel/framework/src/Illuminate/Foundation/",
|
||||
"Illuminate\\Routing\\": "overrides/laravel/framework/src/Illuminate/Routing/",
|
||||
"Illuminate\\Broadcasting\\Broadcasters\\": "overrides/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/",
|
||||
"Illuminate\\Validation\\Concerns\\": "overrides/laravel/framework/src/Illuminate/Validation/Concerns/",
|
||||
"Illuminate\\Mail\\": "overrides/laravel/framework/src/Illuminate/Mail/",
|
||||
"Illuminate\\Auth\\": "overrides/laravel/framework/src/Illuminate/Auth/",
|
||||
"Illuminate\\Auth\\Middleware\\": "overrides/laravel/framework/src/Illuminate/Auth/Middleware/",
|
||||
"Illuminate\\Container\\": "overrides/laravel/framework/src/Illuminate/Container/",
|
||||
"Lord\\Laroute\\Routes\\": "overrides/lord/laroute/src/Routes/",
|
||||
"TorMorten\\Eventy\\": "overrides/tormjens/eventy/src/",
|
||||
"Symfony\\Component\\Debug\\": "overrides/symfony/debug/",
|
||||
"Symfony\\Component\\HttpFoundation\\": "overrides/symfony/http-foundation/",
|
||||
"Chumper\\Zipper\\Repositories\\": "overrides/chumper/zipper/src/Chumper/Zipper/Repositories/",
|
||||
|
||||
"Illuminate\\Support\\": "overrides/laravel/framework/src/Illuminate/Support/",
|
||||
"Illuminate\\Http\\": "overrides/laravel/framework/src/Illuminate/Http/",
|
||||
"Illuminate\\Database\\Eloquent\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/",
|
||||
"Illuminate\\Pagination\\": "overrides/laravel/framework/src/Illuminate/Pagination/",
|
||||
"Illuminate\\Session\\": "overrides/laravel/framework/src/Illuminate/Session/",
|
||||
"Carbon\\": "overrides/nesbot/carbon/src/Carbon/",
|
||||
"Illuminate\\Cache\\": "overrides/laravel/framework/src/Illuminate/Cache/",
|
||||
"Doctrine\\DBAL\\Driver\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Driver/",
|
||||
"Symfony\\Component\\Finder\\Iterator\\": "overrides/symfony/finder/Iterator/",
|
||||
"Symfony\\Component\\Finder\\": "overrides/symfony/finder/",
|
||||
"Symfony\\Component\\Console\\Helper\\": "overrides/symfony/console/Helper/",
|
||||
"DebugBar\\": "overrides/maximebf/debugbar/src/DebugBar/",
|
||||
"Illuminate\\Cache\\Console\\": "overrides/laravel/framework/src/Illuminate/Cache/Console/",
|
||||
"Dotenv\\": "overrides/vlucas/phpdotenv/src/",
|
||||
"Illuminate\\View\\": "overrides/laravel/framework/src/Illuminate/View/",
|
||||
"Symfony\\Component\\Routing\\": "overrides/symfony/routing/",
|
||||
"Symfony\\Component\\VarDumper\\Cloner\\": "overrides/symfony/var-dumper/Cloner/",
|
||||
"Devfactory\\Minify\\Providers\\": "overrides/devfactory/minify/src/Providers/",
|
||||
"Barryvdh\\Debugbar\\": "overrides/barryvdh/laravel-debugbar/src/",
|
||||
"Barryvdh\\Debugbar\\DataFormatter\\": "overrides/barryvdh/laravel-debugbar/src/DataFormatter/",
|
||||
"Symfony\\Component\\Process\\": "overrides/symfony/process/",
|
||||
"Symfony\\Component\\HttpKernel\\Exception\\": "overrides/symfony/http-kernel/Exception/",
|
||||
"Spatie\\String\\": "overrides/spatie/string/src/",
|
||||
"GuzzleHttp\\": "overrides/guzzlehttp/guzzle/src/",
|
||||
"GuzzleHttp\\Cookie\\": "overrides/guzzlehttp/guzzle/src/Cookie/",
|
||||
"Ramsey\\Uuid\\": "overrides/ramsey/uuid/src/",
|
||||
"Rap2hpoutre\\LaravelLogViewer\\": "overrides/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/",
|
||||
"Symfony\\Component\\Console\\Descriptor\\": "overrides/symfony/console/Descriptor",
|
||||
"Symfony\\Component\\CssSelector\\XPath\\Extension\\": "overrides/symfony/css-selector/XPath/Extension/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"vendor/axn/laravel-laroute/src/Routes/Collection.php",
|
||||
"vendor/webklex/laravel-imap/src/IMAP/Message.php",
|
||||
"vendor/webklex/laravel-imap/src/IMAP/Client.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Helpers/RequirementsChecker.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Helpers/PermissionsChecker.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Helpers/EnvironmentManager.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Helpers/FinalInstallManager.php",
|
||||
@@ -120,14 +162,87 @@
|
||||
"vendor/barryvdh/laravel-translation-manager/src/Manager.php",
|
||||
"vendor/barryvdh/laravel-translation-manager/src/Controller.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Validation/Concerns/ValidatesAttributes.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Mail/TransportManager.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Container/Container.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/Broadcaster.php",
|
||||
"vendor/rachidlaasri/laravel-installer/src/Providers/LaravelInstallerServiceProvider.php",
|
||||
"vendor/lord/laroute/src/Routes/Collection.php",
|
||||
"vendor/tormjens/eventy/src/Filter.php",
|
||||
"vendor/tormjens/eventy/src/Action.php",
|
||||
"vendor/symfony/debug/ExceptionHandler.php"
|
||||
"vendor/symfony/debug/ExceptionHandler.php",
|
||||
"vendor/symfony/http-foundation/ResponseHeaderBag.php",
|
||||
"vendor/chumper/zipper/src/Chumper/Zipper/Repositories/ZipRepository.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php",
|
||||
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Collection.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Http/Request.php",
|
||||
"vendor/symfony/http-foundation/ParameterBag.php",
|
||||
"vendor/symfony/http-foundation/HeaderBag.php",
|
||||
"vendor/symfony/http-foundation/Response.php",
|
||||
"vendor/symfony/http-foundation/AcceptHeader.php",
|
||||
"vendor/symfony/http-foundation/FileBag.php",
|
||||
"vendor/symfony/http-foundation/Request.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Pagination/AbstractPaginator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Pagination/Paginator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Pagination/LengthAwarePaginator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Session/FileSessionHandler.php",
|
||||
"vendor/nesbot/carbon/src/Carbon/Carbon.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Cache/Repository.php",
|
||||
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php",
|
||||
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOQueryImplementation.php",
|
||||
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php",
|
||||
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatementImplementations.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Carbon.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Str.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Support/ViewErrorBag.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Support/MessageBag.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Optional.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Fluent.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factory.php",
|
||||
"vendor/symfony/finder/Finder.php",
|
||||
"vendor/symfony/finder/Iterator/FilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/FileTypeFilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php",
|
||||
"vendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/FilenameFilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/PathFilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php",
|
||||
"vendor/symfony/finder/Iterator/DateRangeFilterIterator.php",
|
||||
"vendor/symfony/console/Helper/HelperSet.php",
|
||||
"vendor/maximebf/debugbar/src/DebugBar/DebugBar.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Cache/Console/ClearCommand.php",
|
||||
"vendor/vlucas/phpdotenv/src/Loader.php",
|
||||
"vendor/laravel/framework/src/Illuminate/View/View.php",
|
||||
"vendor/symfony/routing/Route.php",
|
||||
"vendor/symfony/routing/CompiledRoute.php",
|
||||
"vendor/symfony/var-dumper/Cloner/Data.php",
|
||||
"vendor/symfony/var-dumper/Cloner/Stub.php",
|
||||
"vendor/devfactory/minify/src/Providers/BaseProvider.php",
|
||||
"vendor/barryvdh/laravel-debugbar/src/DataFormatter/QueryFormatter.php",
|
||||
"vendor/symfony/process/Process.php",
|
||||
"vendor/symfony/http-kernel/Exception/HttpException.php",
|
||||
"vendor/barryvdh/laravel-debugbar/src/JavascriptRenderer.php",
|
||||
"vendor/spatie/string/src/Str.php",
|
||||
"vendor/guzzlehttp/guzzle/src/Client.php",
|
||||
"vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php",
|
||||
"vendor/ramsey/uuid/src/Uuid.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/Router.php",
|
||||
"vendor/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php",
|
||||
"vendor/symfony/console/Descriptor/TextDescriptor.php",
|
||||
"vendor/symfony/console/Helper/Helper.php",
|
||||
"vendor/symfony/finder/Iterator/SortableIterator.php",
|
||||
"vendor/symfony/css-selector/XPath/Extension/NodeExtension.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
@@ -149,6 +264,8 @@
|
||||
"@php artisan key:generate"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"@php -r \"file_put_contents('vendor/composer/ClassLoader.php', preg_replace('/function includeFile.*/s', base64_decode('DQpmdW5jdGlvbiBpbmNsdWRlRmlsZSgkZmlsZSkNCnsNCiAgICB0cnkgew0KICAgICAgICBpbmNsdWRlICRmaWxlOw0KICAgIH0gY2F0Y2ggKFxFeGNlcHRpb24gJGUpIHsNCiAgICAgICAgJG1zZyA9ICRlLT5nZXRNZXNzYWdlKCk7DQogICAgICAgIGlmIChzdHJzdHIoJG1zZywgJ05vIHN1Y2ggZmlsZSBvciBkaXJlY3RvcnknKSkgew0KICAgICAgICAgICAgaWYgKHN0cnN0cigkbXNnLCAnL3ZlbmRvci9jb21wb3Nlci8uLi8uLi9vdmVycmlkZXMvJykpIHsNCiAgICAgICAgICAgICAgICAkbmV3X2ZpbGUgPSBzdHJfcmVwbGFjZSgnL3ZlbmRvci9jb21wb3Nlci8uLi8uLi9vdmVycmlkZXMvJywgJy92ZW5kb3IvY29tcG9zZXIvLi4vJywgJGZpbGUpOw0KICAgICAgICAgICAgfSBlbHNlIHsNCiAgICAgICAgICAgICAgICAkbmV3X2ZpbGUgPSBzdHJfcmVwbGFjZSgnL3ZlbmRvci9jb21wb3Nlci8uLi8nLCAnL3ZlbmRvci9jb21wb3Nlci8uLi8uLi9vdmVycmlkZXMvJywgJGZpbGUpOw0KICAgICAgICAgICAgfQ0KICAgICAgICAgICAgaW5jbHVkZSAkbmV3X2ZpbGU7DQogICAgICAgIH0gZWxzZSB7DQogICAgICAgICAgICB0aHJvdyAkZTsNCiAgICAgICAgfQ0KICAgIH0NCn0='), file_get_contents('vendor/composer/ClassLoader.php')));\"",
|
||||
"@php -r \"!error_reporting(0) || array_map('unlink', json_decode(file_get_contents('composer.json'), true)['autoload']['exclude-from-classmap']);\"",
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover"
|
||||
]
|
||||
|
||||
1777
composer.lock
generated
1777
composer.lock
generated
File diff suppressed because it is too large
Load Diff
111
config/app.php
111
config/app.php
@@ -1,5 +1,11 @@
|
||||
<?php
|
||||
|
||||
$key = env('APP_KEY', null);
|
||||
$key_file = env("APP_KEY_FILE", null);
|
||||
if (empty($key) && !empty($key_file)) {
|
||||
$key = trim(file_get_contents($key_file));
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -12,7 +18,7 @@ return [
|
||||
| or any other location as required by the application or its packages.
|
||||
*/
|
||||
|
||||
'version' => '1.4.7',
|
||||
'version' => '1.8.19',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -92,7 +98,8 @@ return [
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
'locales' => ['en', 'fr', 'de', 'it', 'pt-PT', 'pt-BR', 'ru', 'nl'],
|
||||
'locales' => ['en', 'hr', 'cs', 'da', 'nl', 'fr', 'de', 'it', 'ja', 'ko', 'fa', 'pl', 'pt-PT', 'pt-BR', 'ru', 'es', 'sk', 'sv'],
|
||||
'locales_rtl' => ['fa'],
|
||||
'default_locale' => 'en',
|
||||
|
||||
/*
|
||||
@@ -125,7 +132,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
'key' => $key,
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
@@ -193,7 +200,7 @@ return [
|
||||
| PHP extensions required by the app
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'required_extensions' => ['mysql / mysqli', 'mbstring', 'xml', 'imap', /*'mcrypt' mcrypt is deprecated*/ 'json', 'gd', 'fileinfo', 'openssl', 'zip', 'tokenizer'/*, 'dom', 'xmlwriter', 'libxml', 'phar'*/],
|
||||
'required_extensions' => ['mysql / mysqli', 'mbstring', 'xml', 'imap', /*'mcrypt' mcrypt is deprecated*/ 'json', 'gd', 'fileinfo', 'openssl', 'zip', 'tokenizer', 'curl'/*, 'dom', 'xmlwriter', 'libxml', 'phar'*/],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -247,6 +254,102 @@ return [
|
||||
'open_tracking' => ['default' => true],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| php - attachments are downloaded via PHP.
|
||||
|
|
||||
| apache - attachments are downloaded via Apache's mod_xsendfile.
|
||||
|
|
||||
| nginx - attachments are downloaded via nginx's X-Accel-Redirect.
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'download_attachments_via' => env('APP_DOWNLOAD_ATTACHMENTS_VIA', 'php'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| File types which should be viewed in the browser instead of downloading.
|
||||
| SVG images are not viewable to avid XSS.
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'viewable_attachments' => env('APP_VIEWABLE_ATTACHMENTS', ['jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'apng', 'bmp', 'gif', 'ico', 'cur', 'png', 'tif', 'tiff', 'webp', 'pdf', 'txt', 'mp3', 'wav', 'ogg', 'wma']),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Case insensitive regular expression, containing a list of
|
||||
| mail server error responses, returned when a mail server can not deliver an email
|
||||
| to one or more recipients. If FreeScout receives one of the listed
|
||||
| error responses from the mail server, it does not try to resend the email
|
||||
| to avoid sending multiple duplicate emails to other recipients.
|
||||
|
|
||||
| https://github.com/freescout-helpdesk/freescout/issues/870#issuecomment-786477909
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC)'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| none - send to the customer only agent's reply in the email.
|
||||
|
|
||||
| last - send to the customer the last message in the email.
|
||||
|
|
||||
| full - send to the customer full conversation history in the email.
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'email_conv_history' => env('APP_EMAIL_CONV_HISTORY', 'none'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| none - send to the user only agent's reply in the email.
|
||||
|
|
||||
| last - send to the user the last message in the email.
|
||||
|
|
||||
| full - send to the user full conversation history in the email.
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'email_user_history' => env('APP_EMAIL_USER_HISTORY', 'full'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JSON containing user permissions.
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'user_permissions' => env('APP_USER_PERMISSIONS', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Dashboard path.
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'dashboard_path' => env('APP_DASHBOARD_PATH', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Dashboard path.
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'login_path' => env('APP_LOGIN_PATH', 'login'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Home page controller.
|
||||
|
|
||||
|-------------------------------------------------------------------------
|
||||
*/
|
||||
'home_controller' => env('APP_HOMEPAGE_CONTROLLER', 'SecureController@dashboard'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Disable update checker
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'disable_updating' => env('APP_DISABLE_UPDATING', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
|
||||
@@ -41,10 +41,10 @@ return [
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'token',
|
||||
'provider' => 'users',
|
||||
],
|
||||
// 'api' => [
|
||||
// 'driver' => 'token',
|
||||
// 'provider' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -96,6 +96,7 @@ return [
|
||||
'provider' => 'users',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ return [
|
||||
|
|
||||
*/
|
||||
'core' => [
|
||||
'minPhpVersion' => '7.0.0',
|
||||
'minPhpVersion' => '7.1.0',
|
||||
'maxPhpVersion' => '8.99.99',
|
||||
],
|
||||
'final' => [
|
||||
'key' => false,
|
||||
@@ -60,6 +61,7 @@ return [
|
||||
'public/css/builds/' => '775',
|
||||
'public/js/builds/' => '775',
|
||||
'public/modules/' => '775',
|
||||
'Modules/' => '775',
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -24,7 +24,7 @@ return [
|
||||
|
||||
'stubs' => [
|
||||
'enabled' => true,
|
||||
'path' => base_path().'/overrides/Nwidart/Modules/Commands/stubs',
|
||||
'path' => base_path().'/overrides/nwidart/laravel-modules/src/Commands/stubs',
|
||||
'files' => [
|
||||
'start' => 'start.php',
|
||||
'routes' => 'Http/routes.php',
|
||||
|
||||
@@ -24,7 +24,7 @@ return [
|
||||
'settings' => [
|
||||
'default' => [
|
||||
'HTML.Doctype' => 'HTML 4.01 Transitional',
|
||||
'HTML.Allowed' => 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table,tr,td,th,thead,tfoot,tbody,blockquote',
|
||||
'HTML.Allowed' => 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table,tr,td,th,thead,tfoot,tbody,blockquote,pre,h1,h2,h3,h4,h5,h6',
|
||||
//'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
|
||||
'CSS.AllowedProperties' => 'font-weight,font-style,text-decoration,color,background-color,text-align',
|
||||
'AutoFormat.AutoParagraph' => true,
|
||||
|
||||
31
config/subscriptions.php
Normal file
31
config/subscriptions.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|-------------------
|
||||
| Default Notification Subscriptions
|
||||
|----------------------
|
||||
|
|
||||
| This file is for configuring the default notification subscriptions for new users.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
\App\Subscription::MEDIUM_EMAIL => [
|
||||
\App\Subscription::EVENT_CONVERSATION_ASSIGNED_TO_ME,
|
||||
\App\Subscription::EVENT_FOLLOWED_CONVERSATION_UPDATED,
|
||||
//\App\Subscription::EVENT_MY_TEAM_MENTIONED,
|
||||
\App\Subscription::EVENT_CUSTOMER_REPLIED_TO_MY,
|
||||
\App\Subscription::EVENT_USER_REPLIED_TO_MY,
|
||||
],
|
||||
\App\Subscription::MEDIUM_BROWSER => [
|
||||
\App\Subscription::EVENT_CONVERSATION_ASSIGNED_TO_ME,
|
||||
\App\Subscription::EVENT_FOLLOWED_CONVERSATION_UPDATED,
|
||||
//\App\Subscription::EVENT_MY_TEAM_MENTIONED,
|
||||
\App\Subscription::EVENT_CUSTOMER_REPLIED_TO_MY,
|
||||
\App\Subscription::EVENT_USER_REPLIED_TO_MY,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -11,7 +11,7 @@ return [
|
||||
|
|
||||
*/
|
||||
'route' => [
|
||||
'prefix' => trim(parse_url(env('APP_URL', ''), PHP_URL_PATH), '/').'/translations',
|
||||
'prefix' => trim(parse_url(env('APP_URL', ''), PHP_URL_PATH) ?: '', '/').'/translations',
|
||||
'middleware' => [
|
||||
'web',
|
||||
'auth',
|
||||
|
||||
@@ -8,6 +8,6 @@ $factory->define(App\Customer::class, function (Faker $faker) {
|
||||
'first_name' => $faker->firstName,
|
||||
'last_name' => $faker->lastName,
|
||||
'job_title' => $faker->jobTitle,
|
||||
'phones' => Customer::formatPhones([['value' => $faker->phoneNumber, 'type' => Customer::PHONE_TYPE_WORK]]),
|
||||
'phones' => json_encode(Customer::formatPhones([['value' => $faker->phoneNumber, 'type' => Customer::PHONE_TYPE_WORK]])),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ class CreatePasswordResetsTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
\Helper::disableSqlRequirePrimaryKey();
|
||||
|
||||
Schema::create('password_resets', function (Blueprint $table) {
|
||||
$table->string('email', 191)->index();
|
||||
$table->string('token', 255);
|
||||
|
||||
@@ -39,7 +39,11 @@ class CreateCustomersTable extends Migration
|
||||
|
||||
// Indexes
|
||||
// For ajax search
|
||||
$table->index([DB::raw('first_name(80)'), DB::raw('last_name(80)')]);
|
||||
if (DB::connection()->getPDO()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
|
||||
$table->index([DB::raw('first_name(80)'), DB::raw('last_name(80)')]);
|
||||
} else {
|
||||
$table->index(['first_name', 'last_name']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,13 @@ class CreateThreadsTable extends Migration
|
||||
$table->timestamp('opened_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
|
||||
$table->unique([DB::raw('message_id(191)')], 'threads_message_id_index');
|
||||
if (DB::connection()->getPDO()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
|
||||
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
|
||||
$table->unique([DB::raw('message_id(191)')], 'threads_message_id_index');
|
||||
} else {
|
||||
$table->unique('message_id', 'threads_message_id_index');
|
||||
}
|
||||
|
||||
// On conversation page
|
||||
$table->index(['conversation_id', 'type', 'from', 'customer_id']);
|
||||
$table->index(['conversation_id', 'created_at']);
|
||||
|
||||
@@ -31,8 +31,13 @@ class CreateSendLogsTable extends Migration
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
|
||||
$table->index([DB::raw('message_id(191)')], 'send_logs_message_id_index');
|
||||
if (DB::connection()->getPDO()->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
|
||||
// https://github.com/laravel/framework/issues/9293#issuecomment-373229281
|
||||
$table->index([DB::raw('message_id(191)')], 'send_logs_message_id_index');
|
||||
} else {
|
||||
$table->index(['message_id'], 'send_logs_message_id_index');
|
||||
}
|
||||
|
||||
// Used when sending auto reply
|
||||
$table->index(['customer_id', 'mail_type', 'created_at']);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ class CreateNotificationsTable extends Migration
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
\Helper::disableSqlRequirePrimaryKey();
|
||||
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('type');
|
||||
|
||||
@@ -14,7 +14,6 @@ class AddInImapFoldersColumnToMailboxesTable extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
// Additional IMAP folders, INBOX folder is fetched by default.
|
||||
$table->text('in_imap_folders')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddHideColumnToMailboxUserTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('mailbox_user', function (Blueprint $table) {
|
||||
// Hide admin from Assign list.
|
||||
$table->boolean('hide')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mailbox_user', function (Blueprint $table) {
|
||||
$table->dropColumn('hide');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddMuteColumnToMailboxUserTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('mailbox_user', function (Blueprint $table) {
|
||||
// Mute notifications for events not directly related to the user
|
||||
$table->boolean('mute')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mailbox_user', function (Blueprint $table) {
|
||||
$table->dropColumn('mute');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddPublicColumnToAttachmentsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('attachments', function (Blueprint $table) {
|
||||
$table->boolean('public')->default(false);
|
||||
});
|
||||
DB::table('attachments')->update(['public' => true]);
|
||||
|
||||
$old_path = storage_path('app'.DIRECTORY_SEPARATOR.'public'.DIRECTORY_SEPARATOR.'attachment');
|
||||
$new_path = storage_path('app'.DIRECTORY_SEPARATOR.'attachment');
|
||||
|
||||
// Move attachments.
|
||||
try {
|
||||
if (File::exists($old_path) && File::isDirectory($old_path) && !File::exists($new_path)) {
|
||||
File::move($old_path, $new_path);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Migration Error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('attachments', function (Blueprint $table) {
|
||||
$table->dropColumn('public');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class UpdateInImapFoldersInMailboxesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Add INBOX to the in_imap_folders.
|
||||
$mailboxes = App\Mailbox::select(['id', 'in_imap_folders'])->get();
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
$in_imap_folders = $mailbox->getInImapFolders();
|
||||
if (count($in_imap_folders) && !in_array('INBOX', $in_imap_folders)) {
|
||||
array_unshift($in_imap_folders, 'INBOX');
|
||||
$mailbox->setInImapFolders($in_imap_folders);
|
||||
$mailbox->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddImapSentFolderColumnToMailboxesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
$table->string('imap_sent_folder', 25)->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
$table->dropColumn('imap_sent_folder');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class DropSlugColumnInMailboxesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// slug field is not used.
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
// Index is dropped automatically.
|
||||
$table->dropColumn('slug');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
$table->string('slug', 16)->unique()->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddEmailHistoryColumnToConversationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('conversations', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('email_history')->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('conversations', function (Blueprint $table) {
|
||||
$table->dropColumn('email_history');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddAccessColumnToMailboxUserTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('mailbox_user', function (Blueprint $table) {
|
||||
$table->text('access')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('mailbox_user', function (Blueprint $table) {
|
||||
$table->dropColumn('access');
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user